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;