From fcc47a78a47bee107de666a2624cb3db926c430c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 14:02:35 -0700 Subject: [PATCH 01/11] update to API v2.0.0 --- manifest.json | 10 +++++----- scripts/classes/Structure.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/manifest.json b/manifest.json index ffc6aa4..901fa1c 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "StrucTool", "description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", - "min_engine_version": [1, 21, 60], + "min_engine_version": [1, 21, 70], "version": [1, 0, 0] }, "modules": [ @@ -26,19 +26,19 @@ "dependencies": [ { "module_name": "@minecraft/server", - "version": "1.18.0-beta" + "version": "2.0.0-beta" }, { "module_name": "@minecraft/server-ui", - "version": "1.4.0-beta" + "version": "2.0.0-beta" }, { "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP - "version": [1, 0, 2] + "version": [1, 0, 3] }, { "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP - "version": [1, 3, 8] + "version": [1, 3, 9] } ], "metadata": { diff --git a/scripts/classes/Structure.js b/scripts/classes/Structure.js index be9735b..5824200 100644 --- a/scripts/classes/Structure.js +++ b/scripts/classes/Structure.js @@ -1,11 +1,11 @@ -import { MinecraftDimensionTypes, world } from "@minecraft/server"; +import { world } from "@minecraft/server"; import { Outliner } from "./Outliner"; export class Structure { #structure; #options = { isPlaced: false, - dimensionId: MinecraftDimensionTypes.overworld, + dimensionId: 'minecraft:overworld', worldLocation: { x: 0, y: 0, z: 0 }, rotation: 0, mirror: false, From 0c90e13e2c200035ac5d90dac92f31bfa343b4b4 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 14:31:30 -0700 Subject: [PATCH 02/11] refactor BlockInfo & change action item to paper --- scripts/classes/BlockInfo.js | 38 +++++++++++++++++++--------------- scripts/rules/easyPlace.js | 12 +++++------ scripts/rules/fastEasyPlace.js | 8 +++---- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js index 5d6c30c..c063592 100644 --- a/scripts/classes/BlockInfo.js +++ b/scripts/classes/BlockInfo.js @@ -1,24 +1,28 @@ import { system, world } from '@minecraft/server'; import { Raycaster } from '../classes/Raycaster'; -system.runInterval(() => { - for (const player of world.getAllPlayers()) { - if (!player) - continue; - showStructureBlockInfo(player); +class BlockInfo { + static onTick() { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + this.showStructureBlockInfo(player); + } } -}); -function showStructureBlockInfo(player) { - const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); - if (!block) - return; - player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); + static showStructureBlockInfo(player) { + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + if (!block) + return; + player.onScreenDisplay.setActionBar({ text: this.getFormattedBlockInfo(block.permutation) }); + } + + static getFormattedBlockInfo(block) { + const states = block.getAllStates(); + if (Object.keys(states).length === 0) + return `Structure:\n§a${block.type.id}`; + return `Structure:\n§a${block.type.id}\n§7${JSON.stringify(block.getAllStates())}`; + } } -function getFormattedBlockInfo(block) { - const states = block.getAllStates(); - if (Object.keys(states).length === 0) - return `Structure:\n§a${block.type.id}`; - return `Structure:\n§a${block.type.id}\n§7${JSON.stringify(block.getAllStates())}`; -} \ No newline at end of file +system.runInterval(() => BlockInfo.onTick()); \ No newline at end of file diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 3a983da..28b744f 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -5,11 +5,11 @@ import { structureCollection } from '../classes/StructureCollection'; import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, blockIdToItemStackMap } from '../data'; -const ARROW_SLOT = 35; +const ACTION_SLOT = 35; const easyPlace = new Rule({ identifier: 'easyPlace', - description: { text: 'Simplifies placing blocks in a structure (arrow in bottom right inventory slot).' }, + description: { text: 'Simplifies placing blocks in a structure (paper in bottom right inventory slot).' }, onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } }) @@ -17,19 +17,19 @@ extension.addRule(easyPlace); function onPlayerPlaceBlock(event) { const { player, block } = event; - if (!player || !block || !hasArrowInCorrectSlot(player)) return; + if (!player || !block || !hasActionItemInCorrectSlot(player)) return; const structureBlock = fetchStructureBlock(block.location); if (!structureBlock) return; tryPlaceBlock(event, player, block, structureBlock); } -function hasArrowInCorrectSlot(player) { +function hasActionItemInCorrectSlot(player) { const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; if (!inventory) return false; - const arrowSlot = inventory.getSlot(ARROW_SLOT); - return arrowSlot.hasItem() && arrowSlot.typeId === 'minecraft:arrow'; + const actionSlot = inventory.getSlot(ACTION_SLOT); + return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper'; } function fetchStructureBlock(location) { diff --git a/scripts/rules/fastEasyPlace.js b/scripts/rules/fastEasyPlace.js index c7c15de..f941bda 100644 --- a/scripts/rules/fastEasyPlace.js +++ b/scripts/rules/fastEasyPlace.js @@ -8,7 +8,7 @@ import { Raycaster } from '../classes/Raycaster'; let runner = void 0; const easyPlace = new Rule({ identifier: 'fastEasyPlace', - description: { text: 'Looking at structure blocks with an arrow in your hand will place them.' }, + description: { text: 'Looking at structure blocks with paper in your hand will place them.' }, onEnableCallback: () => { runner = system.runInterval(onTick, 2); }, onDisableCallback: () => { system.clearRun(runner); } }) @@ -23,7 +23,7 @@ function onTick() { } function processEasyPlace(player) { - if (!player || !isHoldingArrow(player)) return; + if (!player || !isHoldingActionItem(player)) return; const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true }); if (!structureBlock) return; @@ -31,11 +31,11 @@ function processEasyPlace(player) { tryPlaceBlock(player, worldBlock, structureBlock.permutation); } -function isHoldingArrow(player) { +function isHoldingActionItem(player) { const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand); if (!mainhandItemStack) return false; - return mainhandItemStack.typeId === 'minecraft:arrow'; + return mainhandItemStack.typeId === 'minecraft:paper'; } function tryPlaceBlock(player, worldBlock, structureBlock) { From e43fb49d484e613248b10d6bc7b8652f80838d00 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 19:53:42 -0700 Subject: [PATCH 03/11] form start --- README.md | 3 +- scripts/classes/InstanceEditForm.js | 116 ++++++++++++++++++ scripts/classes/InstanceEditFormBuilder.js | 7 ++ scripts/classes/InstanceEditOptions.js | 15 +++ scripts/classes/MenuForm.js | 79 ++++++++++++ scripts/classes/MenuFormBuilder.js | 43 +++++++ scripts/classes/Raycaster.js | 1 - scripts/classes/StructureCollection.js | 53 +++++--- .../{Structure.js => StructureInstance.js} | 61 +++++---- scripts/commands/menu.js | 26 ++++ scripts/commands/struct.js | 10 +- scripts/main.js | 1 + scripts/utils.js | 5 +- 13 files changed, 372 insertions(+), 48 deletions(-) create mode 100644 scripts/classes/InstanceEditForm.js create mode 100644 scripts/classes/InstanceEditFormBuilder.js create mode 100644 scripts/classes/InstanceEditOptions.js create mode 100644 scripts/classes/MenuForm.js create mode 100644 scripts/classes/MenuFormBuilder.js rename scripts/classes/{Structure.js => StructureInstance.js} (68%) create mode 100644 scripts/commands/menu.js diff --git a/README.md b/README.md index e467fef..8a56fc4 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,12 @@ Removes a structure. ## Roadmap -- [ ] place structure **(in progress)** +- [ ] place structure using form **(in progress)** - [x] easyPlace rule - [x] Make a viewable material list - [ ] Make automatic material gathering from inventories - [ ] structure movement & rotation - [ ] structuraMode & holoprintMode rules for ease of use -- [ ] send structure as a scriptevent so that other addons can use it - [ ] more litematica features! ## Issues & Suggestions diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js new file mode 100644 index 0000000..d50d972 --- /dev/null +++ b/scripts/classes/InstanceEditForm.js @@ -0,0 +1,116 @@ +import { structureCollection } from './StructureCollection'; +import { MenuForm } from '../classes/MenuForm'; +import { ActionFormData } from '@minecraft/server-ui'; +import { InstanceEditOptions } from './InstanceEditOptions'; + +export class InstanceEditForm { + instanceName; + options = { + isPlaced: [ + InstanceEditOptions.NextLayer, + InstanceEditOptions.PreviousLayer, + InstanceEditOptions.SetLayer, + InstanceEditOptions.Move, + InstanceEditOptions.Rotate, + InstanceEditOptions.Mirror, + InstanceEditOptions.RemovePlacement, + ], + notPlaced: [ + InstanceEditOptions.PlaceInstance + ], + common: [ + InstanceEditOptions.MaterialsList, + InstanceEditOptions.RenameInstance, + InstanceEditOptions.DeleteInstance, + InstanceEditOptions.MainMenu + ] + } + + constructor(player, instanceName) { + this.player = player; + this.instanceName = instanceName; + this.instance = structureCollection.get(this.instanceName); + this.show(); + } + + show() { + const form = this.buildInstanceForm(); + form.show(this.player).then((response) => { + if (response.canceled) return; + let selectedOption; + if (this.instance.isPlaced()) + selectedOption = this.options.isPlaced[response.selection] || this.options.common[response.selection-this.options.isPlaced.length]; + else + selectedOption = this.options.notPlaced[response.selection] || this.options.common[response.selection-this.options.notPlaced.length]; + this.handleOption(selectedOption); + }); + } + + buildInstanceForm() { + const instanceOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced; + const form = new ActionFormData() + .title('§l§2StrucTool §8Menu') + .body(`Instance: §2${this.instanceName}`) + instanceOptions.forEach(option => { + form.button(`${option}`); + }); + this.options.common.forEach(option => { + form.button(`${option}`); + }); + return form; + } + + handleOption(option) { + switch (option) { + case InstanceEditOptions.PlaceInstance: + this.instance.place(this.player.dimension.id, this.player.location); + break; + case InstanceEditOptions.RemovePlacement: + this.instance.removePlacement(); + break; + case InstanceEditOptions.RenameInstance: + this.renameInstanceForm(); + break; + case InstanceEditOptions.DeleteInstance: + this.structureCollection.remove(this.instanceName); + break; + case InstanceEditOptions.NextLayer: + this.instance.setLayer(this.instance.getLayer() + 1); + new InstanceEditForm(this.player, this.instanceName); + break; + case InstanceEditOptions.PreviousLayer: + this.instance.setLayer(this.instance.getLayer() - 1); + new InstanceEditForm(this.player, this.instanceName); + break; + case InstanceEditOptions.SetLayer: + this.setLayerForm(); + break; + case InstanceEditOptions.Rotate: + this.player.sendMessage('§cRotating not implemented yet.'); + break; + case InstanceEditOptions.Mirror: + this.player.sendMessage('§cMirroring not implemented yet.'); + break; + case InstanceEditOptions.Move: + this.instance.move(this.player.dimension.id, this.player.location); + break; + case InstanceEditOptions.MaterialsList: + this.player.sendMessage('§cMaterial list not implemented yet.'); + break; + case InstanceEditOptions.MainMenu: + new MenuForm(this.player); + break; + default: + this.player.sendMessage(`§cUnknown option: ${option}`); + break; + } + } + + renameInstanceForm() { + + } + + setLayerForm() { + // should have a toggle for if it should layer the structure or notw + } +} \ No newline at end of file diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js new file mode 100644 index 0000000..5b39a61 --- /dev/null +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -0,0 +1,7 @@ +import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; + +export class InstanceEditFormBuilder { + static buildRenameInstance() { + + } +} \ No newline at end of file diff --git a/scripts/classes/InstanceEditOptions.js b/scripts/classes/InstanceEditOptions.js new file mode 100644 index 0000000..c67c8be --- /dev/null +++ b/scripts/classes/InstanceEditOptions.js @@ -0,0 +1,15 @@ +export const InstanceEditOptions = Object.freeze({ + Unknown: "unknown", + MainMenu: 'Back to Main Menu', + PlaceInstance: 'Place Instance', + RemovePlacement: 'Remove Placement', + RenameInstance: 'Rename Instance', + DeleteInstance: '§cDelete Instance', + NextLayer: 'Increase Layer', + PreviousLayer: 'Decrease Layer', + SetLayer: 'Set Layer', + Move: 'Move Here', + Rotate: 'Rotate', + Mirror: 'Mirror', + MaterialsList: 'Get Materials List', +}); \ No newline at end of file diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js new file mode 100644 index 0000000..0e60e93 --- /dev/null +++ b/scripts/classes/MenuForm.js @@ -0,0 +1,79 @@ +import { forceShow } from '../utils'; +import { structureCollection } from './StructureCollection'; +import { MenuFormBuilder } from './MenuFormBuilder'; +import { InstanceEditForm } from './InstanceEditForm'; + +export class MenuForm { + constructor(player) { + this.player = player; + this.show(); + } + + async show() { + let instanceName = this.getInstanceFromLocation(); + if (!instanceName) + instanceName = await this.getInstanceNameFromForm(); + if (!instanceName) + return; + new InstanceEditForm(this.player, instanceName); + } + + getInstanceFromLocation() { + const locatedStructures = structureCollection.getStructuresAtLocation(this.player.location); + if (locatedStructures.length === 0) + return void 0; + const structure = locatedStructures[0]; + return structure.instanceName; + } + + async getInstanceNameFromForm() { + try { + return forceShow(this.player, MenuFormBuilder.buildAllInstanceNameForm()).then((response) => { + if (response.canceled) return; + const selectedInstanceName = structureCollection.getInstanceNames()[response.selection]; + return selectedInstanceName || this.createNewInstance(); + }); + } catch (e) { + if (e.message === 'Menu timed out.') { + this.player.sendMessage('§8Menu timed out.'); + return; + } + throw e; + } + } + + async createNewInstance() { + return MenuFormBuilder.buildNewInstanceForm().show(this.player).then(async (response) => { + if (response.canceled) + return; + const instanceName = response.formValues[0]; + if (instanceName === '') + return void 0; + const structureId = await this.getStructureId(); + if (!structureId) + return; + structureCollection.add(instanceName, structureId); + return instanceName; + }); + } + + async getStructureId() { + return MenuFormBuilder.buildAllStructuresForm().show(this.player).then((response) => { + if (response.canceled) + return; + const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; + return selectedStructureId || this.getOtherStructureId(); + }); + } + + getOtherStructureId() { + return MenuFormBuilder.buildOtherStructureForm().show(this.player).then((response) => { + if (response.canceled) + return; + const structureId = response.formValues[0]; + if (structureId === '') + return void 0; + return structureId; + }); + } +} \ No newline at end of file diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js new file mode 100644 index 0000000..01422e3 --- /dev/null +++ b/scripts/classes/MenuFormBuilder.js @@ -0,0 +1,43 @@ +import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; +import { structureCollection } from './StructureCollection'; + +export class MenuFormBuilder { + static menuTitle = '§l§2StrucTool §8Menu'; + + static buildAllInstanceNameForm() { + const allInstanceNameForm = new ActionFormData() + .title(this.menuTitle) + .body('Select an instance:'); + structureCollection.getInstanceNames().forEach(instanceName => { + allInstanceNameForm.button(`§2${instanceName}`); + }); + allInstanceNameForm.button('Create New Instance'); + return allInstanceNameForm; + } + + static buildNewInstanceForm() { + return new ModalFormData() + .title(this.menuTitle) + .textField('Enter a name for the new instance:', 'example_instance') + .submitButton('Submit'); + } + + static buildAllStructuresForm() { + const allStructuresForm = new ActionFormData() + .title(this.menuTitle) + .body('Select a structure:'); + structureCollection.getWorldStructureIds().forEach(structureId => { + const structureName = structureId.replace('mystructure:', ''); + allStructuresForm.button(`§2${structureName}`); + }); + allStructuresForm.button('Other'); + return allStructuresForm; + } + + static buildOtherStructureForm() { + return new ModalFormData() + .title(this.menuTitle) + .textField('Enter the Structure ID:', 'example_structure') + .submitButton('Submit'); + } +} \ No newline at end of file diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js index bbe64e9..befb420 100644 --- a/scripts/classes/Raycaster.js +++ b/scripts/classes/Raycaster.js @@ -1,4 +1,3 @@ -import { world } from "@minecraft/server"; import { structureCollection } from "./StructureCollection"; export class Raycaster { diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index c558a04..781b07e 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -1,37 +1,41 @@ -import { Structure } from './Structure'; +import { StructureInstance } from './StructureInstance'; +import { world } from '@minecraft/server'; class StructureCollection { - #structures; + structures; constructor() { - this.#structures = {}; + this.structures = {}; } - add(name) { - if (this.#structures[name]) { - throw new Error(`Structure ${name} already exists.`); - } - const structure = new Structure(name); - this.#structures[name] = structure; + add(instanceName, structureId) { + if (this.structures[instanceName]) + throw new Error(`Instance ${instanceName} already exists.`); + const structure = new StructureInstance(instanceName, structureId); + this.structures[instanceName] = structure; return structure; } - get(name) { - const structure = this.#structures[name]; + get(instanceName) { + const structure = this.structures[instanceName]; if (!structure) { - throw new Error(`Structure ${name} not found.`); + throw new Error(`Instance ${instanceName} not found.`); } return structure; } - remove(name) { - const struct = this.get(name); - struct.remove(); - delete this.#structures[name]; + remove(instanceName) { + const struct = this.get(instanceName); + struct.removePlacement(); + delete this.structures[instanceName]; + } + + getInstanceNames() { + return Object.keys(this.structures); } getStructuresAtLocation(location) { - return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); + return Object.values(this.structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); } fetchStructureBlock(location) { @@ -41,6 +45,21 @@ class StructureCollection { const structure = locatedStructures[0]; return structure.getBlock(structure.toStructureCoords(location)); } + + getWorldStructureIds() { + return world.structureManager.getWorldStructureIds() + .filter(id => id.startsWith('mystructure:')) + .map(id => id.replace('mystructure:', '')); + } + + rename(instanceName, newName) { + const structure = this.get(instanceName); + if (this.structures[newName]) + throw new Error(`Instance ${newName} already exists.`); + this.structures[newName] = structure; + delete this.structures[instanceName]; + structure.name = newName; + } } export const structureCollection = new StructureCollection(); \ No newline at end of file diff --git a/scripts/classes/Structure.js b/scripts/classes/StructureInstance.js similarity index 68% rename from scripts/classes/Structure.js rename to scripts/classes/StructureInstance.js index 5824200..6ee2252 100644 --- a/scripts/classes/Structure.js +++ b/scripts/classes/StructureInstance.js @@ -1,7 +1,7 @@ import { world } from "@minecraft/server"; import { Outliner } from "./Outliner"; -export class Structure { +export class StructureInstance { #structure; #options = { isPlaced: false, @@ -12,13 +12,15 @@ export class Structure { currentLayer: 0 }; - constructor(structureName) { - this.name = structureName; - this.#structure = world.structureManager.get(structureName); + constructor(instanceName, structureId) { + this.name = instanceName; + this.structureId = structureId; + this.#structure = world.structureManager.get(structureId); if (!this.#structure) { - throw new Error(`[StrucTool] Structure '${structureName}' not found.`); + throw new Error(`[StrucTool] Structure '${this.structureId}' not found.`); } this.#options = this.loadOptions(); + this.#options.isPlaced = false; } loadOptions() { @@ -39,7 +41,7 @@ export class Structure { } getLocation() { - return this.#options.worldLocation; + return { dimensionId: this.#options.dimensionId, location: this.#options.worldLocation }; } getHeight() { @@ -47,7 +49,7 @@ export class Structure { } getLayer() { - return this.#options.currentLayer; + return this.#options.currentLayer || 0; } *getBlocks() { @@ -83,7 +85,7 @@ export class Structure { getLayeredBounds() { if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); return { min: { x: 0, y: this.#options.currentLayer - 1, z: 0 }, max: { x: this.#structure.size.x, y: this.#options.currentLayer, z: this.#structure.size.z } @@ -91,31 +93,44 @@ export class Structure { } place(dimensionId, worldLocation) { - this.#options = { - isPlaced: true, - dimensionId, - worldLocation: { x: Math.floor(worldLocation.x), y: Math.floor(worldLocation.y), z: Math.floor(worldLocation.z) }, - }; - this.updateOptions(); - this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); + this.#options.isPlaced = true; + this.move(dimensionId, worldLocation); } - remove() { + removePlacement() { if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); this.#options.isPlaced = false; this.updateOptions(); this.outliner.stopDraw(); } + move(dimensionId, location) { + if (!this.#options.isPlaced) + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); + this.#options.dimensionId = dimensionId; + this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }; + this.updateOptions(); + this.refreshOutliner(); + } + setLayer(layer) { if (layer < 1 || layer > this.#structure.size.y) - throw new Error(`[StrucTool] Structure '${this.name}' does not have layer ${layer}.`); + throw new Error(`[StrucTool] Instance '${this.name}' of '${this.structureId}' does not have layer ${layer}.`); this.#options.currentLayer = layer; this.updateOptions(); - this.outliner.stopDraw(); - const { min, max } = this.getLayeredBounds(); - this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + this.refreshOutliner(); + } + + refreshOutliner() { + if (this.outliner) + this.outliner.stopDraw(); + if (this.#options.currentLayer > 0) { + const { min, max } = this.getLayeredBounds(); + this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + } else { + this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); + } } isLocationInStructure(structureLocation) { @@ -155,4 +170,8 @@ export class Structure { z: worldLocation.z - this.#options.worldLocation.z }; } + + isPlaced() { + return this.#options.isPlaced; + } } \ No newline at end of file diff --git a/scripts/commands/menu.js b/scripts/commands/menu.js new file mode 100644 index 0000000..fe20ea4 --- /dev/null +++ b/scripts/commands/menu.js @@ -0,0 +1,26 @@ +import { Command } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { structureCollection } from '../classes/StructureCollection'; +import { MaterialCounter } from '../classes/MaterialCounter'; +import { world, system } from '@minecraft/server'; +import { MenuForm } from '../classes/MenuForm'; + +const ACTION_ITEM = 'minecraft:paper'; + +const structCmd = new Command({ + name: 'menu', + description: { text: 'Manages current StrucTool structures.' }, + usage: 'menu', + callback: structCommand +}); +extension.addCommand(structCmd); + +world.beforeEvents.itemUse.subscribe((event) => { + if (!event.source || event.itemStack?.typeId !== ACTION_ITEM) return; + event.cancel = true; + system.run(() => structCommand(event.source)); +}); + +function structCommand(sender) { + new MenuForm(sender); +} \ No newline at end of file diff --git a/scripts/commands/struct.js b/scripts/commands/struct.js index b3664d0..320f4af 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -17,7 +17,7 @@ const structCmd = new Command({ extension.addCommand(structCmd); function structCommand(sender, args) { - const { option, name, arg3 } = args; + const { name, option, arg3 } = args; switch (option) { case 'add': addStructure(sender, name); @@ -41,7 +41,7 @@ function structCommand(sender, args) { function addStructure(sender, name) { try { - structureCollection.add(name); + structureCollection.add(name, name); } catch (e) { if (e.message.includes('already exists')) { sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); @@ -70,7 +70,7 @@ function placeStructure(sender, name) { structure = structureCollection.get(name); } catch (e) { try { - structure = structureCollection.add(name); + structure = structureCollection.add(name, name); } catch (e) { if (e.message.includes('already exists')) { sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); @@ -108,8 +108,8 @@ function printInfo(sender, name) { sender.sendMessage({ text: `§cStructure '${name}' not found.` }); return; } - const location = structure.getLocation(); - sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}]` }); + const { dimensionId, location } = structure.getLocation(); + sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}] in '${dimensionId}'` }); sender.sendMessage({ text: `§7Current Layer: ${structure.getLayer()}` }); sender.sendMessage({ text: `§7Materials: ${MaterialCounter.getPrintable(name)}` }); } \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index 9b6d634..d7a4547 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -4,6 +4,7 @@ import './rules/fastEasyPlace'; // Commands import './commands/struct'; +import './commands/menu'; // Other import './classes/BlockInfo'; diff --git a/scripts/utils.js b/scripts/utils.js index 6cf2187..49caa4e 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,3 +1,4 @@ +import { system } from '@minecraft/server'; import { FormCancelationReason } from '@minecraft/server-ui'; export async function forceShow(player, form, timeout = Infinity) { @@ -5,9 +6,9 @@ export async function forceShow(player, form, timeout = Infinity) { while ((system.currentTick - startTick) < timeout) { const response = await form.show(player); if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy) - player.sendMessage({ translate: 'commands.canopy.menu.busy' }); + player.sendMessage("§8Close your chat window to access the menu."); if (response.cancelationReason !== FormCancelationReason.UserBusy) return response; } - throw new Error({ translate: 'commands.canopy.menu.timeout', with: [String(timeout)] }); + throw new Error("Menu timed out."); }; \ No newline at end of file From 8da6474b38a2758355c78d460c483ddabcae6a9c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 20:05:10 -0700 Subject: [PATCH 04/11] nicer formatting for structure blockstates --- scripts/classes/BlockInfo.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js index c063592..57ca8eb 100644 --- a/scripts/classes/BlockInfo.js +++ b/scripts/classes/BlockInfo.js @@ -21,7 +21,11 @@ class BlockInfo { const states = block.getAllStates(); if (Object.keys(states).length === 0) return `Structure:\n§a${block.type.id}`; - return `Structure:\n§a${block.type.id}\n§7${JSON.stringify(block.getAllStates())}`; + return `Structure:\n§a${block.type.id}\n§7${this.getFormattedStates(states)}`; + } + + static getFormattedStates(states) { + return Object.entries(states).map(([key, value]) => `§7${key}: ${value}`).join('\n'); } } From 6ced22b57b2d493e88244f5d31269619a0b5b8da Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 20:56:24 -0700 Subject: [PATCH 05/11] setLayer form and make easyPlace rules require paper named "easyPlace" --- scripts/classes/InstanceEditForm.js | 25 +++++++------------- scripts/classes/InstanceEditFormBuilder.js | 27 +++++++++++++++++++++- scripts/classes/MenuForm.js | 8 +++---- scripts/classes/MenuFormBuilder.js | 8 +++---- scripts/classes/StructureInstance.js | 4 ++-- scripts/commands/menu.js | 2 -- scripts/rules/easyPlace.js | 4 ++-- scripts/rules/fastEasyPlace.js | 4 ++-- 8 files changed, 48 insertions(+), 34 deletions(-) diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js index d50d972..6adc11d 100644 --- a/scripts/classes/InstanceEditForm.js +++ b/scripts/classes/InstanceEditForm.js @@ -2,6 +2,7 @@ import { structureCollection } from './StructureCollection'; import { MenuForm } from '../classes/MenuForm'; import { ActionFormData } from '@minecraft/server-ui'; import { InstanceEditOptions } from './InstanceEditOptions'; +import { InstanceEditFormBuilder } from './InstanceEditFormBuilder'; export class InstanceEditForm { instanceName; @@ -34,8 +35,8 @@ export class InstanceEditForm { } show() { - const form = this.buildInstanceForm(); - form.show(this.player).then((response) => { + const currentOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced; + InstanceEditFormBuilder.buildInstance(this.instanceName, currentOptions, this.options.common).show(this.player).then((response) => { if (response.canceled) return; let selectedOption; if (this.instance.isPlaced()) @@ -46,20 +47,6 @@ export class InstanceEditForm { }); } - buildInstanceForm() { - const instanceOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced; - const form = new ActionFormData() - .title('§l§2StrucTool §8Menu') - .body(`Instance: §2${this.instanceName}`) - instanceOptions.forEach(option => { - form.button(`${option}`); - }); - this.options.common.forEach(option => { - form.button(`${option}`); - }); - return form; - } - handleOption(option) { switch (option) { case InstanceEditOptions.PlaceInstance: @@ -111,6 +98,10 @@ export class InstanceEditForm { } setLayerForm() { - // should have a toggle for if it should layer the structure or notw + InstanceEditFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => { + if (response.canceled) return; + const selectedLayer = response.formValues[0]; + this.instance.setLayer(parseInt(selectedLayer)); + }); } } \ No newline at end of file diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js index 5b39a61..4700e76 100644 --- a/scripts/classes/InstanceEditFormBuilder.js +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -1,7 +1,32 @@ import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; +import { MenuFormBuilder } from './MenuFormBuilder'; export class InstanceEditFormBuilder { + static buildInstance(instanceName, currentOptions, commonOptions) { + const form = new ActionFormData() + .title(MenuFormBuilder.menuTitle) + .body(`Instance: §2${instanceName}`) + currentOptions.forEach(option => { + form.button(`${option}`); + }); + commonOptions.forEach(option => { + form.button(`${option}`); + }); + return form; + } + static buildRenameInstance() { - + return new ModalFormData() + .title(MenuFormBuilder.menuTitle) + .textField('Enter a new name for the instance:', 'example_instance') + .submitButton('Rename'); + } + + static buildSetLayer(maxLayer, currentLayer) { + return new ModalFormData() + .title(MenuFormBuilder.menuTitle) + .label('Use the slider to select the layer. Use 0 for all layers.') + .slider("Layer", 0, maxLayer, 1, currentLayer) + .submitButton('Set Layer'); } } \ No newline at end of file diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js index 0e60e93..89d03a3 100644 --- a/scripts/classes/MenuForm.js +++ b/scripts/classes/MenuForm.js @@ -28,7 +28,7 @@ export class MenuForm { async getInstanceNameFromForm() { try { - return forceShow(this.player, MenuFormBuilder.buildAllInstanceNameForm()).then((response) => { + return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => { if (response.canceled) return; const selectedInstanceName = structureCollection.getInstanceNames()[response.selection]; return selectedInstanceName || this.createNewInstance(); @@ -43,7 +43,7 @@ export class MenuForm { } async createNewInstance() { - return MenuFormBuilder.buildNewInstanceForm().show(this.player).then(async (response) => { + return MenuFormBuilder.buildNewInstance().show(this.player).then(async (response) => { if (response.canceled) return; const instanceName = response.formValues[0]; @@ -58,7 +58,7 @@ export class MenuForm { } async getStructureId() { - return MenuFormBuilder.buildAllStructuresForm().show(this.player).then((response) => { + return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => { if (response.canceled) return; const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; @@ -67,7 +67,7 @@ export class MenuForm { } getOtherStructureId() { - return MenuFormBuilder.buildOtherStructureForm().show(this.player).then((response) => { + return MenuFormBuilder.buildOtherStructure().show(this.player).then((response) => { if (response.canceled) return; const structureId = response.formValues[0]; diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js index 01422e3..e07cf7e 100644 --- a/scripts/classes/MenuFormBuilder.js +++ b/scripts/classes/MenuFormBuilder.js @@ -4,7 +4,7 @@ import { structureCollection } from './StructureCollection'; export class MenuFormBuilder { static menuTitle = '§l§2StrucTool §8Menu'; - static buildAllInstanceNameForm() { + static buildAllInstanceName() { const allInstanceNameForm = new ActionFormData() .title(this.menuTitle) .body('Select an instance:'); @@ -15,14 +15,14 @@ export class MenuFormBuilder { return allInstanceNameForm; } - static buildNewInstanceForm() { + static buildNewInstance() { return new ModalFormData() .title(this.menuTitle) .textField('Enter a name for the new instance:', 'example_instance') .submitButton('Submit'); } - static buildAllStructuresForm() { + static buildAllStructures() { const allStructuresForm = new ActionFormData() .title(this.menuTitle) .body('Select a structure:'); @@ -34,7 +34,7 @@ export class MenuFormBuilder { return allStructuresForm; } - static buildOtherStructureForm() { + static buildOtherStructure() { return new ModalFormData() .title(this.menuTitle) .textField('Enter the Structure ID:', 'example_structure') diff --git a/scripts/classes/StructureInstance.js b/scripts/classes/StructureInstance.js index 6ee2252..f5e5feb 100644 --- a/scripts/classes/StructureInstance.js +++ b/scripts/classes/StructureInstance.js @@ -115,7 +115,7 @@ export class StructureInstance { } setLayer(layer) { - if (layer < 1 || layer > this.#structure.size.y) + if (layer < 0 || layer > this.#structure.size.y) throw new Error(`[StrucTool] Instance '${this.name}' of '${this.structureId}' does not have layer ${layer}.`); this.#options.currentLayer = layer; this.updateOptions(); @@ -129,7 +129,7 @@ export class StructureInstance { const { min, max } = this.getLayeredBounds(); this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); } else { - this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); + this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); } } diff --git a/scripts/commands/menu.js b/scripts/commands/menu.js index fe20ea4..02c06fb 100644 --- a/scripts/commands/menu.js +++ b/scripts/commands/menu.js @@ -1,7 +1,5 @@ import { Command } from '../lib/canopy/CanopyExtension'; import { extension } from '../config'; -import { structureCollection } from '../classes/StructureCollection'; -import { MaterialCounter } from '../classes/MaterialCounter'; import { world, system } from '@minecraft/server'; import { MenuForm } from '../classes/MenuForm'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 28b744f..d05e116 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -9,7 +9,7 @@ const ACTION_SLOT = 35; const easyPlace = new Rule({ identifier: 'easyPlace', - description: { text: 'Simplifies placing blocks in a structure (paper in bottom right inventory slot).' }, + description: { text: "Simplifies placing blocks in a structure (paper named 'easyPlace' in bottom right inventory slot)." }, onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } }) @@ -29,7 +29,7 @@ function hasActionItemInCorrectSlot(player) { if (!inventory) return false; const actionSlot = inventory.getSlot(ACTION_SLOT); - return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper'; + return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace'; } function fetchStructureBlock(location) { diff --git a/scripts/rules/fastEasyPlace.js b/scripts/rules/fastEasyPlace.js index f941bda..28a04bb 100644 --- a/scripts/rules/fastEasyPlace.js +++ b/scripts/rules/fastEasyPlace.js @@ -8,7 +8,7 @@ import { Raycaster } from '../classes/Raycaster'; let runner = void 0; const easyPlace = new Rule({ identifier: 'fastEasyPlace', - description: { text: 'Looking at structure blocks with paper in your hand will place them.' }, + description: { text: "Looking at structure blocks with paper named 'easyPlace' in your hand will place them." }, onEnableCallback: () => { runner = system.runInterval(onTick, 2); }, onDisableCallback: () => { system.clearRun(runner); } }) @@ -35,7 +35,7 @@ function isHoldingActionItem(player) { const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand); if (!mainhandItemStack) return false; - return mainhandItemStack.typeId === 'minecraft:paper'; + return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'easyPlace'; } function tryPlaceBlock(player, worldBlock, structureBlock) { From 373b3df8538b3ed810f0ede7bea8ba0c39c72d4d Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 01:11:23 -0700 Subject: [PATCH 06/11] Form complete but not error handled --- README.md | 10 +- scripts/classes/BlockInfo.js | 14 ++- scripts/classes/InstanceEditForm.js | 88 +++++++++------- scripts/classes/InstanceEditFormBuilder.js | 17 +-- scripts/classes/InstanceEditOptions.js | 14 ++- scripts/classes/MenuForm.js | 24 +++-- scripts/classes/Raycaster.js | 7 +- scripts/classes/StructureCollection.js | 34 ++++-- scripts/classes/StructureInstance.js | 116 ++++++++++++++++----- scripts/rules/easyPlace.js | 10 +- scripts/rules/fastEasyPlace.js | 8 +- 11 files changed, 225 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 8a56fc4..d1a7a58 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,11 @@ Removes a structure. ## Roadmap -- [ ] place structure using form **(in progress)** +- [x] Form to manage structure - [x] easyPlace rule -- [x] Make a viewable material list -- [ ] Make automatic material gathering from inventories -- [ ] structure movement & rotation -- [ ] structuraMode & holoprintMode rules for ease of use -- [ ] more litematica features! +- [ ] Automatic material gathering from inventories +- [ ] Structure movement & rotation +- [ ] More litematica features! ## Issues & Suggestions diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js index 57ca8eb..70c512f 100644 --- a/scripts/classes/BlockInfo.js +++ b/scripts/classes/BlockInfo.js @@ -2,6 +2,8 @@ import { system, world } from '@minecraft/server'; import { Raycaster } from '../classes/Raycaster'; class BlockInfo { + static shownToLastTick = new Set(); + static onTick() { for (const player of world.getAllPlayers()) { if (!player) @@ -12,16 +14,24 @@ class BlockInfo { static showStructureBlockInfo(player) { const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + if (!block && this.shownToLastTick.has(player.id)) { + player.onScreenDisplay.setActionBar({ text: 'Structure:\n§7None' }); + this.shownToLastTick.delete(player.id); + } if (!block) return; player.onScreenDisplay.setActionBar({ text: this.getFormattedBlockInfo(block.permutation) }); + this.shownToLastTick.add(player.id); } static getFormattedBlockInfo(block) { + const header = 'Structure:\n' + if (!block) + return header + '§7Unknown'; const states = block.getAllStates(); if (Object.keys(states).length === 0) - return `Structure:\n§a${block.type.id}`; - return `Structure:\n§a${block.type.id}\n§7${this.getFormattedStates(states)}`; + return header + `§a${block.type.id}`; + return header + `§a${block.type.id}\n§7${this.getFormattedStates(states)}`; } static getFormattedStates(states) { diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js index 6adc11d..ec68058 100644 --- a/scripts/classes/InstanceEditForm.js +++ b/scripts/classes/InstanceEditForm.js @@ -1,27 +1,28 @@ import { structureCollection } from './StructureCollection'; import { MenuForm } from '../classes/MenuForm'; -import { ActionFormData } from '@minecraft/server-ui'; import { InstanceEditOptions } from './InstanceEditOptions'; import { InstanceEditFormBuilder } from './InstanceEditFormBuilder'; export class InstanceEditForm { instanceName; - options = { - isPlaced: [ + #buttons = { + isEnabled: [ InstanceEditOptions.NextLayer, InstanceEditOptions.PreviousLayer, InstanceEditOptions.SetLayer, InstanceEditOptions.Move, - InstanceEditOptions.Rotate, - InstanceEditOptions.Mirror, - InstanceEditOptions.RemovePlacement, + InstanceEditOptions.RenameInstance, + InstanceEditOptions.DisableInstance, ], - notPlaced: [ - InstanceEditOptions.PlaceInstance + isNotEnabledAndIsNotPlaced: [ + InstanceEditOptions.PlaceInstance, + InstanceEditOptions.RenameInstance + ], + isNotEnabledButIsPlaced: [ + InstanceEditOptions.EnableInstance, + InstanceEditOptions.RenameInstance ], common: [ - InstanceEditOptions.MaterialsList, - InstanceEditOptions.RenameInstance, InstanceEditOptions.DeleteInstance, InstanceEditOptions.MainMenu ] @@ -35,57 +36,65 @@ export class InstanceEditForm { } show() { - const currentOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced; - InstanceEditFormBuilder.buildInstance(this.instanceName, currentOptions, this.options.common).show(this.player).then((response) => { + const currentOptions = this.getActiveOptions(); + InstanceEditFormBuilder.buildInstance(this.instance, currentOptions).show(this.player).then((response) => { if (response.canceled) return; - let selectedOption; - if (this.instance.isPlaced()) - selectedOption = this.options.isPlaced[response.selection] || this.options.common[response.selection-this.options.isPlaced.length]; - else - selectedOption = this.options.notPlaced[response.selection] || this.options.common[response.selection-this.options.notPlaced.length]; - this.handleOption(selectedOption); + this.handleOption(currentOptions[response.selection]); }); } + getActiveOptions() { + let currentOptions = []; + if (this.instance.isEnabled()) + currentOptions = this.#buttons.isEnabled; + else if (this.instance.hasLocation()) + currentOptions = this.#buttons.isNotEnabledButIsPlaced; + else + currentOptions = this.#buttons.isNotEnabledAndIsNotPlaced; + currentOptions = currentOptions.concat(this.#buttons.common); + + if (!this.instance.hasLayers()) + currentOptions = currentOptions.filter(option => + option !== InstanceEditOptions.SetLayer + && option !== InstanceEditOptions.NextLayer + && option !== InstanceEditOptions.PreviousLayer + ); + return currentOptions; + } + handleOption(option) { switch (option) { + case InstanceEditOptions.EnableInstance: + this.instance.enable(); + break; + case InstanceEditOptions.DisableInstance: + this.instance.disable(); + break; case InstanceEditOptions.PlaceInstance: this.instance.place(this.player.dimension.id, this.player.location); break; - case InstanceEditOptions.RemovePlacement: - this.instance.removePlacement(); - break; case InstanceEditOptions.RenameInstance: this.renameInstanceForm(); break; case InstanceEditOptions.DeleteInstance: - this.structureCollection.remove(this.instanceName); + structureCollection.delete(this.instanceName); break; case InstanceEditOptions.NextLayer: - this.instance.setLayer(this.instance.getLayer() + 1); + this.instance.increaseLayer(); new InstanceEditForm(this.player, this.instanceName); break; case InstanceEditOptions.PreviousLayer: - this.instance.setLayer(this.instance.getLayer() - 1); + this.instance.decreaseLayer(); new InstanceEditForm(this.player, this.instanceName); break; case InstanceEditOptions.SetLayer: this.setLayerForm(); break; - case InstanceEditOptions.Rotate: - this.player.sendMessage('§cRotating not implemented yet.'); - break; - case InstanceEditOptions.Mirror: - this.player.sendMessage('§cMirroring not implemented yet.'); - break; case InstanceEditOptions.Move: this.instance.move(this.player.dimension.id, this.player.location); break; - case InstanceEditOptions.MaterialsList: - this.player.sendMessage('§cMaterial list not implemented yet.'); - break; case InstanceEditOptions.MainMenu: - new MenuForm(this.player); + new MenuForm(this.player, { jumpToInstance: false }); break; default: this.player.sendMessage(`§cUnknown option: ${option}`); @@ -94,7 +103,16 @@ export class InstanceEditForm { } renameInstanceForm() { - + InstanceEditFormBuilder.buildRenameInstance(this.instanceName).show(this.player).then((response) => { + if (response.canceled) return; + const newName = response.formValues[0]; + if (newName === '') { + this.player.sendMessage('§cInstance name cannot be empty.'); + return; + } + structureCollection.rename(this.instanceName, newName); + this.instanceName = newName; + }); } setLayerForm() { diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js index 4700e76..5e66dec 100644 --- a/scripts/classes/InstanceEditFormBuilder.js +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -2,23 +2,24 @@ import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; import { MenuFormBuilder } from './MenuFormBuilder'; export class InstanceEditFormBuilder { - static buildInstance(instanceName, currentOptions, commonOptions) { + static buildInstance(instance, options) { + const location = instance.getLocation(); const form = new ActionFormData() .title(MenuFormBuilder.menuTitle) - .body(`Instance: §2${instanceName}`) - currentOptions.forEach(option => { - form.button(`${option}`); - }); - commonOptions.forEach(option => { + let body = `Instance: §2${instance.name}\n`; + if (instance.hasLocation()) + body += `§7(${location.location.x} ${location.location.y} ${location.location.z} in ${location.dimensionId})\n`; + form.body(body); + options.forEach(option => { form.button(`${option}`); }); return form; } - static buildRenameInstance() { + static buildRenameInstance(currentName) { return new ModalFormData() .title(MenuFormBuilder.menuTitle) - .textField('Enter a new name for the instance:', 'example_instance') + .textField('Enter a new name for the instance:', currentName) .submitButton('Rename'); } diff --git a/scripts/classes/InstanceEditOptions.js b/scripts/classes/InstanceEditOptions.js index c67c8be..7b4fa3b 100644 --- a/scripts/classes/InstanceEditOptions.js +++ b/scripts/classes/InstanceEditOptions.js @@ -1,15 +1,13 @@ export const InstanceEditOptions = Object.freeze({ - Unknown: "unknown", - MainMenu: 'Back to Main Menu', - PlaceInstance: 'Place Instance', - RemovePlacement: 'Remove Placement', + Unknown: 'Unknown', + MainMenu: '<<', + PlaceInstance: '§aPlace Instance', + EnableInstance: '§aEnable Instance', + DisableInstance: '§cDisable Instance', RenameInstance: 'Rename Instance', DeleteInstance: '§cDelete Instance', NextLayer: 'Increase Layer', PreviousLayer: 'Decrease Layer', SetLayer: 'Set Layer', - Move: 'Move Here', - Rotate: 'Rotate', - Mirror: 'Mirror', - MaterialsList: 'Get Materials List', + Move: 'Move Here' }); \ No newline at end of file diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js index 89d03a3..93e6ae0 100644 --- a/scripts/classes/MenuForm.js +++ b/scripts/classes/MenuForm.js @@ -4,26 +4,32 @@ import { MenuFormBuilder } from './MenuFormBuilder'; import { InstanceEditForm } from './InstanceEditForm'; export class MenuForm { - constructor(player) { + constructor(player, { jumpToInstance = true } = {}) { this.player = player; - this.show(); + this.show(jumpToInstance); } - async show() { - let instanceName = this.getInstanceFromLocation(); - if (!instanceName) - instanceName = await this.getInstanceNameFromForm(); + async show(jumpToInstance = true) { + let instanceName; + if (jumpToInstance) { + instanceName = this.getInstanceNameAtLocation(); + if (instanceName) { + new InstanceEditForm(this.player, instanceName); + return; + } + } + instanceName = await this.getInstanceNameFromForm(); if (!instanceName) return; new InstanceEditForm(this.player, instanceName); } - getInstanceFromLocation() { - const locatedStructures = structureCollection.getStructuresAtLocation(this.player.location); + getInstanceNameAtLocation() { + const locatedStructures = structureCollection.getStructures(this.player.dimension.id, this.player.location, { useLayers: false }); if (locatedStructures.length === 0) return void 0; const structure = locatedStructures[0]; - return structure.instanceName; + return structure.name; } async getInstanceNameFromForm() { diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js index befb420..7dfb350 100644 --- a/scripts/classes/Raycaster.js +++ b/scripts/classes/Raycaster.js @@ -1,4 +1,5 @@ import { structureCollection } from "./StructureCollection"; +import { world } from "@minecraft/server"; export class Raycaster { static STEP_SIZE = 0.2; @@ -9,12 +10,10 @@ export class Raycaster { let location = startLocation; let distance = 0; while (distance < maxDistance) { - const locatedStructures = structureCollection.getStructuresAtLocation(location); + const locatedStructures = structureCollection.getStructures(dimension.id, location); if (locatedStructures.length !== 0) { const structure = locatedStructures[0]; const block = structure.getBlock(structure.toStructureCoords(location)); - if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir) - break; if (block?.type.id !== 'minecraft:air') { blocks.push({ permutation: block, @@ -23,6 +22,8 @@ export class Raycaster { if (getFirst) break; } + if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir) + break; } location = { x: location.x + (direction.x*this.STEP_SIZE), diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index 781b07e..605f6e9 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -8,6 +8,21 @@ class StructureCollection { this.structures = {}; } + loadExistingStructures() { + world.getDynamicPropertyIds().filter(id => id.startsWith('structOptions:')).forEach(id => { + const instanceName = id.replace('structOptions:', ''); + let structureId; + try { + structureId = StructureInstance.parseOptions(instanceName).structureId; + this.structures[instanceName] = new StructureInstance(instanceName, structureId); + } catch (e) { + world.sendMessage(`§c[StrucTool] Error loading structure instance '${instanceName}'. It will be removed.`); + world.setDynamicProperty(id, void 0); + throw e; + } + }); + } + add(instanceName, structureId) { if (this.structures[instanceName]) throw new Error(`Instance ${instanceName} already exists.`); @@ -24,9 +39,9 @@ class StructureCollection { return structure; } - remove(instanceName) { + delete(instanceName) { const struct = this.get(instanceName); - struct.removePlacement(); + struct.delete(); delete this.structures[instanceName]; } @@ -34,12 +49,12 @@ class StructureCollection { return Object.keys(this.structures); } - getStructuresAtLocation(location) { - return Object.values(this.structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); + getStructures(dimensionId, location, options = {}) { + return Object.values(this.structures).filter(structure => structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options)); } - fetchStructureBlock(location) { - const locatedStructures = this.getStructuresAtLocation(location); + fetchStructureBlock(dimensionId, location) { + const locatedStructures = this.getStructures(dimensionId, location); if (locatedStructures.length === 0) return void 0; const structure = locatedStructures[0]; @@ -56,10 +71,15 @@ class StructureCollection { const structure = this.get(instanceName); if (this.structures[newName]) throw new Error(`Instance ${newName} already exists.`); + structure.rename(newName); this.structures[newName] = structure; delete this.structures[instanceName]; structure.name = newName; } } -export const structureCollection = new StructureCollection(); \ No newline at end of file +export const structureCollection = new StructureCollection(); + +world.afterEvents.worldLoad.subscribe(() => { + structureCollection.loadExistingStructures(); +}); \ No newline at end of file diff --git a/scripts/classes/StructureInstance.js b/scripts/classes/StructureInstance.js index f5e5feb..fd273d9 100644 --- a/scripts/classes/StructureInstance.js +++ b/scripts/classes/StructureInstance.js @@ -2,10 +2,12 @@ import { world } from "@minecraft/server"; import { Outliner } from "./Outliner"; export class StructureInstance { + name; #structure; #options = { - isPlaced: false, - dimensionId: 'minecraft:overworld', + structureId: void 0, + isEnabled: false, + dimensionId: void 0, worldLocation: { x: 0, y: 0, z: 0 }, rotation: 0, mirror: false, @@ -14,13 +16,14 @@ export class StructureInstance { constructor(instanceName, structureId) { this.name = instanceName; - this.structureId = structureId; this.#structure = world.structureManager.get(structureId); - if (!this.#structure) { - throw new Error(`[StrucTool] Structure '${this.structureId}' not found.`); - } + if (!this.#structure) + throw new Error(`[StrucTool] Structure '${structureId}' not found.`); this.#options = this.loadOptions(); - this.#options.isPlaced = false; + this.#options.structureId = structureId; + if (this.#options.isEnabled) + this.refreshOutliner(); + this.updateOptions(); } loadOptions() { @@ -32,6 +35,21 @@ export class StructureInstance { return this.#options; } + delete() { + this.disable(); + world.setDynamicProperty(`structOptions:${this.name}`, void 0); + this.#structure = void 0; + this.#options = void 0; + delete this.outliner; + } + + static parseOptions(instanceName) { + const options = JSON.parse(world.getDynamicProperty(`structOptions:${instanceName}`)); + if (!options) + throw new Error(`[StrucTool] Instance '${instanceName}' not found.`); + return options; + } + updateOptions() { world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options)); } @@ -84,7 +102,7 @@ export class StructureInstance { } getLayeredBounds() { - if (!this.#options.isPlaced) + if (!this.#options.isEnabled) throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); return { min: { x: 0, y: this.#options.currentLayer - 1, z: 0 }, @@ -92,22 +110,30 @@ export class StructureInstance { }; } - place(dimensionId, worldLocation) { - this.#options.isPlaced = true; - this.move(dimensionId, worldLocation); + rename(newName) { + world.setDynamicProperty(`structOptions:${this.name}`, void 0); + this.name = newName; + world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options)); } - removePlacement() { - if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); - this.#options.isPlaced = false; + place(dimensionId, worldLocation) { + this.move(dimensionId, worldLocation); + this.enable(); + } + + enable() { + this.#options.isEnabled = true; + this.updateOptions(); + this.refreshOutliner(); + } + + disable() { + this.#options.isEnabled = false; this.updateOptions(); this.outliner.stopDraw(); } move(dimensionId, location) { - if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); this.#options.dimensionId = dimensionId; this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }; this.updateOptions(); @@ -116,13 +142,15 @@ export class StructureInstance { setLayer(layer) { if (layer < 0 || layer > this.#structure.size.y) - throw new Error(`[StrucTool] Instance '${this.name}' of '${this.structureId}' does not have layer ${layer}.`); + throw new Error(`[StrucTool] Layer ${layer} is out of bounds.`); this.#options.currentLayer = layer; this.updateOptions(); this.refreshOutliner(); } refreshOutliner() { + if (!this.#options.isEnabled) + return; if (this.outliner) this.outliner.stopDraw(); if (this.#options.currentLayer > 0) { @@ -133,26 +161,30 @@ export class StructureInstance { } } - isLocationInStructure(structureLocation) { + isLocationInStructure(dimensionId, structureLocation) { + if (this.#options.dimensionId !== dimensionId) + return false const { min, max } = this.getBounds(); return structureLocation.x >= min.x && structureLocation.x < max.x && structureLocation.y >= min.y && structureLocation.y < max.y && structureLocation.z >= min.z && structureLocation.z < max.z; } - isLocationInLayer(structureLocation) { + isLocationInLayer(dimensionId, structureLocation) { + if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId) + return false const { min, max } = this.getLayeredBounds(true); return structureLocation.x >= min.x && structureLocation.x < max.x && structureLocation.y >= min.y && structureLocation.y < max.y && structureLocation.z >= min.z && structureLocation.z < max.z; } - isLocationActive(structureLocation) { - if (!this.#options.isPlaced) + isLocationActive(dimensionId, structureLocation, { useLayers = true } = {}) { + if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId) return false - if (this.#options.currentLayer > 0) - return this.isLocationInLayer(structureLocation); - return this.isLocationInStructure(structureLocation); + if (useLayers && this.#options.currentLayer !== 0) + return this.isLocationInLayer(dimensionId, structureLocation); + return this.isLocationInStructure(dimensionId, structureLocation); } toGlobalCoords(structureLocation) { @@ -171,7 +203,37 @@ export class StructureInstance { }; } - isPlaced() { - return this.#options.isPlaced; + isEnabled() { + return this.#options.isEnabled; + } + + hasLocation() { + return this.#options.dimensionId && this.#options.worldLocation.x !== 0 && this.#options.worldLocation.y !== 0 && this.#options.worldLocation.z !== 0; + } + + hasLayers() { + return this.#structure.size.y > 1; + } + + isAtMaxLayer() { + return !this.hasLayers || this.#options.currentLayer >= this.#structure.size.y; + } + + isAtMinLayer() { + return !this.hasLayers || this.#options.currentLayer <= 0; + } + + increaseLayer() { + if (this.isAtMaxLayer()) + this.setLayer(0); + else + this.setLayer(this.#options.currentLayer + 1); + } + + decreaseLayer() { + if (this.isAtMinLayer()) + this.setLayer(this.#structure.size.y); + else + this.setLayer(this.#options.currentLayer - 1); } } \ No newline at end of file diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index d05e116..5d15a5c 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -18,7 +18,7 @@ extension.addRule(easyPlace); function onPlayerPlaceBlock(event) { const { player, block } = event; if (!player || !block || !hasActionItemInCorrectSlot(player)) return; - const structureBlock = fetchStructureBlock(block.location); + const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location); if (!structureBlock) return; tryPlaceBlock(event, player, block, structureBlock); @@ -32,14 +32,6 @@ function hasActionItemInCorrectSlot(player) { return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace'; } -function fetchStructureBlock(location) { - const locatedStructures = structureCollection.getStructuresAtLocation(location); - if (locatedStructures.length === 0) - return void 0; - const structure = locatedStructures[0]; - return structure.getBlock(structure.toStructureCoords(location)); -} - function tryPlaceBlock(event, player, block, structureBlock) { if (isBannedBlock(player, structureBlock)) return; structureBlock = tryConvertBannedToValidBlock(structureBlock); diff --git a/scripts/rules/fastEasyPlace.js b/scripts/rules/fastEasyPlace.js index 28a04bb..df7b4b1 100644 --- a/scripts/rules/fastEasyPlace.js +++ b/scripts/rules/fastEasyPlace.js @@ -8,7 +8,7 @@ import { Raycaster } from '../classes/Raycaster'; let runner = void 0; const easyPlace = new Rule({ identifier: 'fastEasyPlace', - description: { text: "Looking at structure blocks with paper named 'easyPlace' in your hand will place them." }, + description: { text: "Looking at structure blocks with a paper named 'easyPlace' in your hand will place them." }, onEnableCallback: () => { runner = system.runInterval(onTick, 2); }, onDisableCallback: () => { system.clearRun(runner); } }) @@ -39,7 +39,7 @@ function isHoldingActionItem(player) { } function tryPlaceBlock(player, worldBlock, structureBlock) { - if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(worldBlock)) return; + if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock)) return; structureBlock = tryConvertBannedToValidBlock(structureBlock); if (player.getGameMode() === GameMode.creative) { placeBlock(worldBlock, structureBlock); @@ -49,11 +49,13 @@ function tryPlaceBlock(player, worldBlock, structureBlock) { } } -function locationIsPlaceable(worldBlock) { +function locationIsPlaceable(player, worldBlock) { return worldBlock.isAir; } function isBannedBlock(player, structureBlock) { + if (!structureBlock) + return true; const blockId = structureBlock.type.id.replace('minecraft:', ''); if (bannedBlocks.includes(blockId)) return true; From 79b22e5fd7e16c1a8fa35f0a89e963856ce64799 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 01:13:16 -0700 Subject: [PATCH 07/11] change color of block states --- scripts/classes/BlockInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js index 70c512f..c80d964 100644 --- a/scripts/classes/BlockInfo.js +++ b/scripts/classes/BlockInfo.js @@ -35,7 +35,7 @@ class BlockInfo { } static getFormattedStates(states) { - return Object.entries(states).map(([key, value]) => `§7${key}: ${value}`).join('\n'); + return Object.entries(states).map(([key, value]) => `§7${key}: §3${value}`).join('\n'); } } From 77994b26dd0cb688ed50ecda68cea1e976ad701f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 01:50:15 -0700 Subject: [PATCH 08/11] How to add structures screen --- scripts/classes/BlockInfo.js | 2 +- scripts/classes/MenuForm.js | 5 +++++ scripts/classes/MenuFormBuilder.js | 11 +++++++++++ scripts/classes/Raycaster.js | 8 ++++---- scripts/classes/StructureInstance.js | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js index c80d964..c93e56b 100644 --- a/scripts/classes/BlockInfo.js +++ b/scripts/classes/BlockInfo.js @@ -13,7 +13,7 @@ class BlockInfo { } static showStructureBlockInfo(player) { - const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useLayers: false }); if (!block && this.shownToLastTick.has(player.id)) { player.onScreenDisplay.setActionBar({ text: 'Structure:\n§7None' }); this.shownToLastTick.delete(player.id); diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js index 93e6ae0..8827156 100644 --- a/scripts/classes/MenuForm.js +++ b/scripts/classes/MenuForm.js @@ -2,6 +2,7 @@ import { forceShow } from '../utils'; import { structureCollection } from './StructureCollection'; import { MenuFormBuilder } from './MenuFormBuilder'; import { InstanceEditForm } from './InstanceEditForm'; +import { world } from '@minecraft/server'; export class MenuForm { constructor(player, { jumpToInstance = true } = {}) { @@ -36,6 +37,10 @@ export class MenuForm { try { return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => { if (response.canceled) return; + if (response.selection === structureCollection.getInstanceNames().length + 1) { + MenuFormBuilder.buildHowToAddNewStructures().show(this.player); + return; + } const selectedInstanceName = structureCollection.getInstanceNames()[response.selection]; return selectedInstanceName || this.createNewInstance(); }); diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js index e07cf7e..8716a91 100644 --- a/scripts/classes/MenuFormBuilder.js +++ b/scripts/classes/MenuFormBuilder.js @@ -12,6 +12,7 @@ export class MenuFormBuilder { allInstanceNameForm.button(`§2${instanceName}`); }); allInstanceNameForm.button('Create New Instance'); + allInstanceNameForm.button('How to Add New Structures'); return allInstanceNameForm; } @@ -40,4 +41,14 @@ export class MenuFormBuilder { .textField('Enter the Structure ID:', 'example_structure') .submitButton('Submit'); } + + static buildHowToAddNewStructures() { + let body = "How to Add Structures:\n" + body += "§7- Save a structure using a structure block or the /structure command.\n" + body += "§7OR\n" + body += "§7- Add a mcstructure file to this pack's structures folder. It will not appear in the list of structures, so enter the filename (without '.mcstructure') as the Structure ID."; + return new ActionFormData() + .title(this.menuTitle) + .body(body); + } } \ No newline at end of file diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js index 7dfb350..fdeb365 100644 --- a/scripts/classes/Raycaster.js +++ b/scripts/classes/Raycaster.js @@ -4,13 +4,13 @@ import { world } from "@minecraft/server"; export class Raycaster { static STEP_SIZE = 0.2; - static getStructureBlocks(dimension, startLocation, direction, { maxDistance, getFirst = true, collideWithWorldBlocks = true }) { + static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useLayers = true }) { // Can probably be optimized by the fact that we only need full blocks and aren't checking for partial blocks const blocks = []; let location = startLocation; let distance = 0; while (distance < maxDistance) { - const locatedStructures = structureCollection.getStructures(dimension.id, location); + const locatedStructures = structureCollection.getStructures(dimension.id, location, { useLayers }); if (locatedStructures.length !== 0) { const structure = locatedStructures[0]; const block = structure.getBlock(structure.toStructureCoords(location)); @@ -36,11 +36,11 @@ export class Raycaster { return blocks; } - static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true }) { + static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, useLayers = true } = {}) { const startLocation = player.getHeadLocation(); const direction = player.getViewDirection(); const maxDistance = 7; - const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks }); + const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks, useLayers }); if (blocks.length === 0) return void 0; return isFirst ? blocks[0] : blocks[blocks.length - 1]; diff --git a/scripts/classes/StructureInstance.js b/scripts/classes/StructureInstance.js index fd273d9..a8e19cb 100644 --- a/scripts/classes/StructureInstance.js +++ b/scripts/classes/StructureInstance.js @@ -19,6 +19,7 @@ export class StructureInstance { this.#structure = world.structureManager.get(structureId); if (!this.#structure) throw new Error(`[StrucTool] Structure '${structureId}' not found.`); + this.#structure.saveToWorld(); this.#options = this.loadOptions(); this.#options.structureId = structureId; if (this.#options.isEnabled) From 516f26e2b22fcc2a0e7620f4bb5c25d4eef03c85 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 13:36:16 -0700 Subject: [PATCH 09/11] Add "action prevented by easyPlace" message --- scripts/rules/easyPlace.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 5d15a5c..0c88a38 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -33,7 +33,8 @@ function hasActionItemInCorrectSlot(player) { } function tryPlaceBlock(event, player, block, structureBlock) { - if (isBannedBlock(player, structureBlock)) return; + if (isBannedBlock(player, structureBlock)) + preventAction(event, player); structureBlock = tryConvertBannedToValidBlock(structureBlock); if (player.getGameMode() === GameMode.creative) { placeBlock(block, structureBlock); @@ -43,6 +44,13 @@ function tryPlaceBlock(event, player, block, structureBlock) { } } +function preventAction(event, player) { + event.cancel = true; + system.run(() => { + player.onScreenDisplay.setActionBar('§cAction prevented by easyPlace.'); + }); +} + function isBannedBlock(player, structureBlock) { const blockId = structureBlock.type.id.replace('minecraft:', ''); if (bannedBlocks.includes(blockId)) From b74362166f010e56e03fc0415bc7dfb664caf251 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 13:39:58 -0700 Subject: [PATCH 10/11] update roadmap --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1a7a58..9ba36fb 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,13 @@ Removes a structure. ## Roadmap - [x] Form to manage structure +- [x] Structure naming & movement - [x] easyPlace rule +- [ ] Automatic Armor stand posing - [ ] Automatic material gathering from inventories -- [ ] Structure movement & rotation -- [ ] More litematica features! +- [ ] Correct block placement checking +- [ ] Structure Mirroring & Rotation +- [ ] Structure Merging into SuperStructures ## Issues & Suggestions From 1a56192682251328191a702f3a278da6d11968ce Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 13 Apr 2025 14:27:41 -0700 Subject: [PATCH 11/11] Form Error handling & small code cleanup --- scripts/classes/InstanceEditForm.js | 9 +++++++-- scripts/classes/InstanceEditFormBuilder.js | 2 +- scripts/classes/MenuForm.js | 11 +---------- scripts/classes/MenuFormBuilder.js | 2 +- scripts/classes/Raycaster.js | 15 ++++++++++----- scripts/classes/StructureCollection.js | 11 +++++++---- scripts/classes/StructureInstance.js | 4 ++++ 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js index ec68058..c4acdb7 100644 --- a/scripts/classes/InstanceEditForm.js +++ b/scripts/classes/InstanceEditForm.js @@ -110,8 +110,13 @@ export class InstanceEditForm { this.player.sendMessage('§cInstance name cannot be empty.'); return; } - structureCollection.rename(this.instanceName, newName); - this.instanceName = newName; + try { + structureCollection.rename(this.instanceName, newName); + this.instanceName = newName; + } catch (e) { + this.player.sendMessage(`§cError renaming instance: ${e.message}`); + return; + } }); } diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js index 5e66dec..118a062 100644 --- a/scripts/classes/InstanceEditFormBuilder.js +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -6,7 +6,7 @@ export class InstanceEditFormBuilder { const location = instance.getLocation(); const form = new ActionFormData() .title(MenuFormBuilder.menuTitle) - let body = `Instance: §2${instance.name}\n`; + let body = `Instance: §a${instance.name}\n§fStructure: §2${instance.getStructureId()}\n`; if (instance.hasLocation()) body += `§7(${location.location.x} ${location.location.y} ${location.location.z} in ${location.dimensionId})\n`; form.body(body); diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js index 8827156..d717e75 100644 --- a/scripts/classes/MenuForm.js +++ b/scripts/classes/MenuForm.js @@ -2,7 +2,6 @@ import { forceShow } from '../utils'; import { structureCollection } from './StructureCollection'; import { MenuFormBuilder } from './MenuFormBuilder'; import { InstanceEditForm } from './InstanceEditForm'; -import { world } from '@minecraft/server'; export class MenuForm { constructor(player, { jumpToInstance = true } = {}) { @@ -13,7 +12,7 @@ export class MenuForm { async show(jumpToInstance = true) { let instanceName; if (jumpToInstance) { - instanceName = this.getInstanceNameAtLocation(); + instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useLayers: false })?.name; if (instanceName) { new InstanceEditForm(this.player, instanceName); return; @@ -25,14 +24,6 @@ export class MenuForm { new InstanceEditForm(this.player, instanceName); } - getInstanceNameAtLocation() { - const locatedStructures = structureCollection.getStructures(this.player.dimension.id, this.player.location, { useLayers: false }); - if (locatedStructures.length === 0) - return void 0; - const structure = locatedStructures[0]; - return structure.name; - } - async getInstanceNameFromForm() { try { return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => { diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js index 8716a91..bd7038f 100644 --- a/scripts/classes/MenuFormBuilder.js +++ b/scripts/classes/MenuFormBuilder.js @@ -46,7 +46,7 @@ export class MenuFormBuilder { let body = "How to Add Structures:\n" body += "§7- Save a structure using a structure block or the /structure command.\n" body += "§7OR\n" - body += "§7- Add a mcstructure file to this pack's structures folder. It will not appear in the list of structures, so enter the filename (without '.mcstructure') as the Structure ID."; + body += "§7- Add a .mcstructure file to this pack's structures folder. When selecting your structure, select the 'Other' option and then use the filename (without '.mcstructure') as the Structure ID. After its first use, it will be added to the list of structures."; return new ActionFormData() .title(this.menuTitle) .body(body); diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js index fdeb365..030b58c 100644 --- a/scripts/classes/Raycaster.js +++ b/scripts/classes/Raycaster.js @@ -10,9 +10,8 @@ export class Raycaster { let location = startLocation; let distance = 0; while (distance < maxDistance) { - const locatedStructures = structureCollection.getStructures(dimension.id, location, { useLayers }); - if (locatedStructures.length !== 0) { - const structure = locatedStructures[0]; + const structure = structureCollection.getStructure(dimension.id, location, { useLayers }); + if (structure) { const block = structure.getBlock(structure.toStructureCoords(location)); if (block?.type.id !== 'minecraft:air') { blocks.push({ @@ -22,8 +21,14 @@ export class Raycaster { if (getFirst) break; } - if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir) - break; + try { + if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir) + break; + } catch (e) { + if (e.name === 'LocationOutOfWorldBoundariesError') + break; + throw e; + } } location = { x: location.x + (direction.x*this.STEP_SIZE), diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index 605f6e9..4082036 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -53,11 +53,14 @@ class StructureCollection { return Object.values(this.structures).filter(structure => structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options)); } + getStructure(dimensionId, location, options = {}) { + return this.getStructures(dimensionId, location, options)[0]; + } + fetchStructureBlock(dimensionId, location) { - const locatedStructures = this.getStructures(dimensionId, location); - if (locatedStructures.length === 0) + const structure = this.getStructure(dimensionId, location); + if (!structure) return void 0; - const structure = locatedStructures[0]; return structure.getBlock(structure.toStructureCoords(location)); } @@ -70,7 +73,7 @@ class StructureCollection { rename(instanceName, newName) { const structure = this.get(instanceName); if (this.structures[newName]) - throw new Error(`Instance ${newName} already exists.`); + throw new Error(`Instance '${newName}' already exists.`); structure.rename(newName); this.structures[newName] = structure; delete this.structures[instanceName]; diff --git a/scripts/classes/StructureInstance.js b/scripts/classes/StructureInstance.js index a8e19cb..1b35582 100644 --- a/scripts/classes/StructureInstance.js +++ b/scripts/classes/StructureInstance.js @@ -59,6 +59,10 @@ export class StructureInstance { return this.#structure; } + getStructureId() { + return this.#options.structureId; + } + getLocation() { return { dimensionId: this.#options.dimensionId, location: this.#options.worldLocation }; }