From 21ec447e0e2f073904611bb282469e26b002c43b Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 15 Mar 2025 04:43:11 -0700 Subject: [PATCH 1/4] easyPlace initial --- scripts/classes/Structure.js | 92 ++++++++++++++++---------- scripts/classes/StructureCollection.js | 7 ++ scripts/commands/struct.js | 26 ++++++-- scripts/data.js | 39 +++++++++++ scripts/main.js | 3 +- scripts/rules/easyPlace.js | 77 +++++++++++++++++++++ 6 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 scripts/data.js create mode 100644 scripts/rules/easyPlace.js diff --git a/scripts/classes/Structure.js b/scripts/classes/Structure.js index c743e51..be9735b 100644 --- a/scripts/classes/Structure.js +++ b/scripts/classes/Structure.js @@ -6,7 +6,7 @@ export class Structure { #options = { isPlaced: false, dimensionId: MinecraftDimensionTypes.overworld, - location: { x: 0, y: 0, z: 0 }, + worldLocation: { x: 0, y: 0, z: 0 }, rotation: 0, mirror: false, currentLayer: 0 @@ -39,7 +39,7 @@ export class Structure { } getLocation() { - return this.#options.location; + return this.#options.worldLocation; } getHeight() { @@ -70,51 +70,34 @@ export class Structure { } } - getBlock(location) { - return this.#structure.getBlockPermutation(location); + getBlock(structureLocation) { + return this.#structure.getBlockPermutation(structureLocation); } getBounds() { + return { + min: { x: 0, y: 0, z: 0 }, + max: this.#structure.size + }; + } + + getLayeredBounds() { if (!this.#options.isPlaced) throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); return { - min: { - x: this.#options.location.x, - y: this.#options.location.y, - z: this.#options.location.z - }, - max: { - x: this.#options.location.x + this.#structure.size.x, - y: this.#options.location.y + this.#structure.size.y, - z: this.#options.location.z + this.#structure.size.z - } - } + 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 } + }; } - getLayeredBounds(useUnder = false) { - if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); - if (useUnder) { - return { - min: { x: this.#options.location.x, y: this.#options.location.y, z: this.#options.location.z }, - max: { x: this.#options.location.x + this.#structure.size.x, y: this.#options.location.y + this.#options.currentLayer, z: this.#options.location.z + this.#structure.size.z } - } - } else { - return { - min: { x: this.#options.location.x, y: this.#options.location.y + this.#options.currentLayer - 1,z: this.#options.location.z }, - max: { x: this.#options.location.x + this.#structure.size.x, y: this.#options.location.y + this.#options.currentLayer, z: this.#options.location.z + this.#structure.size.z } - } - } - } - - place(dimensionId, location) { + place(dimensionId, worldLocation) { this.#options = { isPlaced: true, dimensionId, - location: { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }, + worldLocation: { x: Math.floor(worldLocation.x), y: Math.floor(worldLocation.y), z: Math.floor(worldLocation.z) }, }; this.updateOptions(); - this.outliner = new Outliner(dimensionId, this.getBounds().min, this.getBounds().max); + this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); } remove() { @@ -131,6 +114,45 @@ export class Structure { this.#options.currentLayer = layer; this.updateOptions(); this.outliner.stopDraw(); - this.outliner = new Outliner(this.#options.dimensionId, this.getLayeredBounds().min, this.getLayeredBounds().max); + const { min, max } = this.getLayeredBounds(); + this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + } + + isLocationInStructure(structureLocation) { + 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) { + 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) + return false + if (this.#options.currentLayer > 0) + return this.isLocationInLayer(structureLocation); + return this.isLocationInStructure(structureLocation); + } + + toGlobalCoords(structureLocation) { + return { + x: this.#options.worldLocation.x + structureLocation.x, + y: this.#options.worldLocation.y + structureLocation.y, + z: this.#options.worldLocation.z + structureLocation.z + }; + } + + toStructureCoords(worldLocation) { + return { + x: worldLocation.x - this.#options.worldLocation.x, + y: worldLocation.y - this.#options.worldLocation.y, + z: worldLocation.z - this.#options.worldLocation.z + }; } } \ No newline at end of file diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index c393b19..10b838c 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -8,6 +8,9 @@ class StructureCollection { } add(name) { + if (this.#structures[name]) { + throw new Error(`Structure ${name} already exists.`); + } const structure = new Structure(name); this.#structures[name] = structure; return structure; @@ -26,6 +29,10 @@ class StructureCollection { struct.remove(); delete this.#structures[name]; } + + getStructuresAtLocation(location) { + return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); + } } export const structureCollection = new StructureCollection(); \ No newline at end of file diff --git a/scripts/commands/struct.js b/scripts/commands/struct.js index d4cf73e..91a58af 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -7,7 +7,7 @@ import { MaterialCounter } from '../classes/MaterialCounter'; const structCmd = new Command({ name: 'struct', description: { text: 'Manages current StrucTool structures.' }, - usage: 'struct', + usage: 'struct [args...]', callback: structCommand, args: [ { type: 'string', name: 'name' }, @@ -41,7 +41,17 @@ function structCommand(sender, args) { } function addStructure(sender, name) { - structureCollection.add(name); + try { + structureCollection.add(name); + } catch (e) { + if (e.message.includes('already exists')) { + sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); + return; + } else { + sender.sendMessage({ text: `§cFailed to add structure '${name}'.` }); + throw e; + } + } sender.sendMessage({ text: `§7Added structure '${name}'` }); } @@ -63,8 +73,16 @@ function placeStructure(sender, name) { try { structure = structureCollection.add(name); } catch (e) { - sender.sendMessage({ text: `§cStructure '${name}' not found.` }); - return; + if (e.message.includes('already exists')) { + sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); + return; + } else if (e.message.includes('not found')) { + sender.sendMessage({ text: `§cStructure '${name}' not found.` }); + return; + } else { + sender.sendMessage({ text: `§cFailed to place structure '${name}'.` }); + throw e; + } } } structure.place(sender.dimension.id, sender.location); diff --git a/scripts/data.js b/scripts/data.js new file mode 100644 index 0000000..5c8eb22 --- /dev/null +++ b/scripts/data.js @@ -0,0 +1,39 @@ +export const bannedEasyPlaceBlocks = [ + 'air', 'bed', + 'wooden_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', 'mangrove_door', 'cherry_door', 'pale_oak_door', + 'bamboo_door', 'iron_door', 'crimson_door', 'warped_door', 'copper_door', 'exposed_copper_door', 'weathered_copper_door', 'oxidized_copper_door', + 'waxed_copper_door', 'waxed_exposed_copper_door', 'waxed_weathered_copper_door', 'waxed_oxidized_copper_door', + 'piston_arm_collision', 'sticky_piston_arm_collision', "skeleton_skull" +]; + +export const bannedToValidBlockMap = { + 'lit_furnace': 'furnace', + 'lit_smoker': 'smoker', + 'lit_blast_furnace': 'blast_furnace', + 'lit_redstone_ore': 'redstone_ore', + 'unlit_redstone_torch': 'redstone_torch', + 'bubble_column': 'water', + // 'seagrass'? 'tall_seagrass'? kelp? +}; + +export const defaultStates = { + growth: 0, + age: 0, + height: 0, + bite_counter: 0, + fill_level: 0, + redstone_signal: 0, + cluster_count: 0, + respawn_anchor_charge: 0, + turtle_egg_count: "one_egg", + cauldron_liquid: "water" +}; + +export const allowedBlockStates = { + water: { depth_level: 0 }, + lava: { depth_level: 0 } +}; + +export const bannedDimensionBlocks = { + 'nether': ['water', 'bubble_column'] +}; \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index f03bc75..47755ac 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,4 +1,5 @@ // Rules +import './rules/easyPlace'; // Commands -import './commands/struct.js'; +import './commands/struct'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js new file mode 100644 index 0000000..94185fe --- /dev/null +++ b/scripts/rules/easyPlace.js @@ -0,0 +1,77 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { EntityComponentTypes, GameMode, system, world } from '@minecraft/server'; +import { structureCollection } from '../classes/StructureCollection'; +import { bannedEasyPlaceBlocks } from '../data'; + +const easyPlace = new Rule({ + identifier: 'easyPlace', + description: { text: 'Places a structure with ease.' }, + onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, + onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } +}) +extension.addRule(easyPlace); + +function onPlayerPlaceBlock(event) { + const { player, block } = event; + if (!player || !block) return; + const structureBlock = fetchStructureBlock(block.location); + if (!structureBlock || isBannedBlock(structureBlock)) + return; + const states = structureBlock.getAllStates(); + tryPlaceBlock(event, player, block, structureBlock); +} + +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 isBannedBlock(structureBlock) { + return bannedEasyPlaceBlocks.some(bannedId => structureBlock.type.id.replace('minecraft:', '') === bannedId); +} + +function tryPlaceBlock(event, player, block, structureBlock) { + if (player.getGameMode() === GameMode.creative) { + placeBlock(block, structureBlock); + } else if (player.getGameMode() === GameMode.survival) { + tryPlaceBlockSurvival(event, player, block, structureBlock); + } +} + +function tryPlaceBlockSurvival(event, player, block, structureBlock) { + const itemSlotToUse = fetchMatchingItemSlot(player, structureBlock); + if (itemSlotToUse) { + event.cancel = true; + placeBlock(block, structureBlock, itemSlotToUse); + } +} + +function fetchMatchingItemSlot(player, structureBlock) { + const itemToMatch = structureBlock.getItemStack(); + if (!itemToMatch) + return void 0; + const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; + if (!inventory) + return void 0; + for (let index = 0; index < inventory.size; index++) { + const itemSlot = inventory.getSlot(index); + if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatch.typeId) + return itemSlot; + } +} + +function placeBlock(block, structureBlock, itemSlot) { + system.run(() => { + if (itemSlot) { + if (itemSlot.amount === 1) + itemSlot.setItem(void 0); + else + itemSlot.amount--; + } + block.setPermutation(structureBlock); + }); +} \ No newline at end of file From c9927f8e2488165c2b55c1cff90507975c5919d9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 16 Mar 2025 03:34:55 -0700 Subject: [PATCH 2/4] sort out many edge cases for survival --- scripts/data.js | 74 +++++++++++++++++++++++++------- scripts/rules/easyPlace.js | 88 ++++++++++++++++++++++++++++++-------- 2 files changed, 129 insertions(+), 33 deletions(-) diff --git a/scripts/data.js b/scripts/data.js index 5c8eb22..e5f45c9 100644 --- a/scripts/data.js +++ b/scripts/data.js @@ -1,22 +1,32 @@ -export const bannedEasyPlaceBlocks = [ - 'air', 'bed', +export const bannedBlocks = [ + 'air', 'bed', 'piston_arm_collision', 'sticky_piston_arm_collision', "skeleton_skull", 'standing_banner', 'wall_banner', 'wooden_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', 'mangrove_door', 'cherry_door', 'pale_oak_door', 'bamboo_door', 'iron_door', 'crimson_door', 'warped_door', 'copper_door', 'exposed_copper_door', 'weathered_copper_door', 'oxidized_copper_door', 'waxed_copper_door', 'waxed_exposed_copper_door', 'waxed_weathered_copper_door', 'waxed_oxidized_copper_door', - 'piston_arm_collision', 'sticky_piston_arm_collision', "skeleton_skull" + 'seagrass', 'kelp' ]; +export const bannedDimensionBlocks = { + 'overworld': [], + 'nether': ['water'], + 'end': [] +}; + +export const whitelistedBlockStates = { + 'water': { liquid_depth: 0 }, + 'lava': { liquid_depth: 0 } +}; + export const bannedToValidBlockMap = { 'lit_furnace': 'furnace', 'lit_smoker': 'smoker', 'lit_blast_furnace': 'blast_furnace', 'lit_redstone_ore': 'redstone_ore', - 'unlit_redstone_torch': 'redstone_torch', - 'bubble_column': 'water', - // 'seagrass'? 'tall_seagrass'? kelp? + 'lit_redstone_lamp': 'redstone_lamp', + 'unlit_redstone_torch': 'redstone_torch' }; -export const defaultStates = { +export const resetToBlockStates = { growth: 0, age: 0, height: 0, @@ -25,15 +35,49 @@ export const defaultStates = { redstone_signal: 0, cluster_count: 0, respawn_anchor_charge: 0, - turtle_egg_count: "one_egg", - cauldron_liquid: "water" + turtle_egg_count: 0, + cluster_count: 0 }; -export const allowedBlockStates = { - water: { depth_level: 0 }, - lava: { depth_level: 0 } +export const chainedStatePlacements = { + cauldron: { + cauldron_liquid: { + water: ['water_bucket'], + lava: ['lava_bucket'] + } + }, + sea_pickle: { + cluster_count: { + 1: ['sea_pickle'], + 2: ['sea_pickle', 'sea_pickle'], + 3: ['sea_pickle', 'sea_pickle', 'sea_pickle'] + } + }, + turtle_egg: { + turtle_egg_count: { + two_eggs: ['turtle_egg'], + three_eggs: ['turtle_egg', 'turtle_egg'], + four_eggs: ['turtle_egg', 'turtle_egg', 'turtle_egg'] + } + }, + respawn_anchor: { + respawn_anchor_charge: { + 1: ['glowstone'], + 2: ['glowstone', 'glowstone'], + 3: ['glowstone', 'glowstone', 'glowstone'], + 4: ['glowstone', 'glowstone', 'glowstone', 'glowstone'] + } + } }; -export const bannedDimensionBlocks = { - 'nether': ['water', 'bubble_column'] -}; \ No newline at end of file +export const blockIdToItemStackMap = { + 'water': 'water_bucket', + 'lava': 'lava_bucket', + 'fire': 'fire_charge', + 'soul_fire': 'fire_charge', +}; + +export const specialItemPlacementConversions = { + 'water_bucket': 'bucket', + 'lava_bucket': 'bucket' +} \ No newline at end of file diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 94185fe..0bfe8dd 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -1,8 +1,9 @@ import { Rule } from '../lib/canopy/CanopyExtension'; import { extension } from '../config'; -import { EntityComponentTypes, GameMode, system, world } from '@minecraft/server'; +import { BlockPermutation, EntityComponentTypes, GameMode, ItemStack, system, world } from '@minecraft/server'; import { structureCollection } from '../classes/StructureCollection'; -import { bannedEasyPlaceBlocks } from '../data'; +import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, + blockIdToItemStackMap } from '../data'; const easyPlace = new Rule({ identifier: 'easyPlace', @@ -16,9 +17,8 @@ function onPlayerPlaceBlock(event) { const { player, block } = event; if (!player || !block) return; const structureBlock = fetchStructureBlock(block.location); - if (!structureBlock || isBannedBlock(structureBlock)) + if (!structureBlock) return; - const states = structureBlock.getAllStates(); tryPlaceBlock(event, player, block, structureBlock); } @@ -30,36 +30,76 @@ function fetchStructureBlock(location) { return structure.getBlock(structure.toStructureCoords(location)); } -function isBannedBlock(structureBlock) { - return bannedEasyPlaceBlocks.some(bannedId => structureBlock.type.id.replace('minecraft:', '') === bannedId); -} - function tryPlaceBlock(event, player, block, structureBlock) { + if (isBannedBlock(player, structureBlock)) return; + structureBlock = tryConvertBannedToValidBlock(structureBlock); if (player.getGameMode() === GameMode.creative) { placeBlock(block, structureBlock); } else if (player.getGameMode() === GameMode.survival) { + structureBlock = tryConvertToDefaultState(structureBlock); tryPlaceBlockSurvival(event, player, block, structureBlock); } } +function isBannedBlock(player, structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (bannedBlocks.includes(blockId)) + return true; + if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId)) + return true; + const allowedStates = whitelistedBlockStates[blockId]; + if (allowedStates) { + for (const [stateKey, stateValue] of Object.entries(allowedStates)) { + if (structureBlock.getState(stateKey) !== stateValue) + return true; + } + } + return false; +} + +function tryConvertBannedToValidBlock(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (Object.keys(bannedToValidBlockMap).includes(blockId)) + return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates()); + return structureBlock; +} + +function tryConvertToDefaultState(structureBlock) { + const newStates = {}; + for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) { + if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey]) + newStates[stateKey] = resetToBlockStates[stateKey]; + else + newStates[stateKey] = stateValue; + } + return BlockPermutation.resolve(structureBlock.type.id, newStates); +} + function tryPlaceBlockSurvival(event, player, block, structureBlock) { - const itemSlotToUse = fetchMatchingItemSlot(player, structureBlock); + const placeableItemStack = getPlaceableItemStack(structureBlock); + console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); + const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); if (itemSlotToUse) { event.cancel = true; placeBlock(block, structureBlock, itemSlotToUse); } } -function fetchMatchingItemSlot(player, structureBlock) { - const itemToMatch = structureBlock.getItemStack(); - if (!itemToMatch) +function getPlaceableItemStack(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + const newItemId = blockIdToItemStackMap[blockId]; + return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack(); +} + +function fetchMatchingItemSlot(player, itemToMatchId) { + if (!itemToMatchId) return void 0; const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; if (!inventory) return void 0; for (let index = 0; index < inventory.size; index++) { const itemSlot = inventory.getSlot(index); - if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatch.typeId) + if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatchId) return itemSlot; } } @@ -67,11 +107,23 @@ function fetchMatchingItemSlot(player, structureBlock) { function placeBlock(block, structureBlock, itemSlot) { system.run(() => { if (itemSlot) { - if (itemSlot.amount === 1) - itemSlot.setItem(void 0); - else - itemSlot.amount--; + consumeItem(itemSlot); } block.setPermutation(structureBlock); }); -} \ No newline at end of file +} + +function consumeItem(itemSlot) { + if (specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]) { + consumeSpecial(itemSlot); + } else { + if (itemSlot.amount === 1) + itemSlot.setItem(void 0); + else + itemSlot.amount--; + } +} + +function consumeSpecial(itemSlot) { + itemSlot.setItem(new ItemStack(specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')])); +} From fb3deea181412c60ca6767193336285c4e3e2a5d Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 16 Mar 2025 22:46:20 -0700 Subject: [PATCH 3/4] add showStructBlockInfo, fastEasyPlace, and add arrow slot for easyPlace --- scripts/classes/Raycaster.js | 48 +++++++++ scripts/classes/StructureCollection.js | 8 ++ scripts/commands/struct.js | 1 - scripts/data.js | 31 ------ scripts/main.js | 2 + scripts/rules/easyPlace.js | 16 ++- scripts/rules/fastEasyPlace.js | 140 +++++++++++++++++++++++++ scripts/rules/showStructBlockInfo.js | 35 +++++++ 8 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 scripts/classes/Raycaster.js create mode 100644 scripts/rules/fastEasyPlace.js create mode 100644 scripts/rules/showStructBlockInfo.js diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js new file mode 100644 index 0000000..bbe64e9 --- /dev/null +++ b/scripts/classes/Raycaster.js @@ -0,0 +1,48 @@ +import { world } from "@minecraft/server"; +import { structureCollection } from "./StructureCollection"; + +export class Raycaster { + static STEP_SIZE = 0.2; + + static getStructureBlocks(dimension, startLocation, direction, { maxDistance, getFirst = true, collideWithWorldBlocks = 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.getStructuresAtLocation(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, + location: location + }); + if (getFirst) + break; + } + } + location = { + x: location.x + (direction.x*this.STEP_SIZE), + y: location.y + (direction.y*this.STEP_SIZE), + z: location.z + (direction.z*this.STEP_SIZE) + }; + distance += this.STEP_SIZE; + } + // world.getDimension('minecraft:overworld').spawnParticle('minecraft:villager_happy', location); + return blocks; + } + + static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true }) { + const startLocation = player.getHeadLocation(); + const direction = player.getViewDirection(); + const maxDistance = 7; + const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks }); + if (blocks.length === 0) + return void 0; + return isFirst ? blocks[0] : blocks[blocks.length - 1]; + } +} \ No newline at end of file diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index 10b838c..c558a04 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -33,6 +33,14 @@ class StructureCollection { getStructuresAtLocation(location) { return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); } + + fetchStructureBlock(location) { + const locatedStructures = this.getStructuresAtLocation(location); + if (locatedStructures.length === 0) + return void 0; + const structure = locatedStructures[0]; + return structure.getBlock(structure.toStructureCoords(location)); + } } export const structureCollection = new StructureCollection(); \ No newline at end of file diff --git a/scripts/commands/struct.js b/scripts/commands/struct.js index 91a58af..b3664d0 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -1,7 +1,6 @@ import { Command } from '../lib/canopy/CanopyExtension'; import { extension } from '../config'; import { structureCollection } from '../classes/StructureCollection'; -import { system } from '@minecraft/server'; import { MaterialCounter } from '../classes/MaterialCounter'; const structCmd = new Command({ diff --git a/scripts/data.js b/scripts/data.js index e5f45c9..ccb7b80 100644 --- a/scripts/data.js +++ b/scripts/data.js @@ -39,37 +39,6 @@ export const resetToBlockStates = { cluster_count: 0 }; -export const chainedStatePlacements = { - cauldron: { - cauldron_liquid: { - water: ['water_bucket'], - lava: ['lava_bucket'] - } - }, - sea_pickle: { - cluster_count: { - 1: ['sea_pickle'], - 2: ['sea_pickle', 'sea_pickle'], - 3: ['sea_pickle', 'sea_pickle', 'sea_pickle'] - } - }, - turtle_egg: { - turtle_egg_count: { - two_eggs: ['turtle_egg'], - three_eggs: ['turtle_egg', 'turtle_egg'], - four_eggs: ['turtle_egg', 'turtle_egg', 'turtle_egg'] - } - }, - respawn_anchor: { - respawn_anchor_charge: { - 1: ['glowstone'], - 2: ['glowstone', 'glowstone'], - 3: ['glowstone', 'glowstone', 'glowstone'], - 4: ['glowstone', 'glowstone', 'glowstone', 'glowstone'] - } - } -}; - export const blockIdToItemStackMap = { 'water': 'water_bucket', 'lava': 'lava_bucket', diff --git a/scripts/main.js b/scripts/main.js index 47755ac..da757bb 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,5 +1,7 @@ // Rules import './rules/easyPlace'; +import './rules/fastEasyPlace'; +import './rules/showStructBlockInfo'; // Commands import './commands/struct'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 0bfe8dd..3a983da 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -5,9 +5,11 @@ import { structureCollection } from '../classes/StructureCollection'; import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, blockIdToItemStackMap } from '../data'; +const ARROW_SLOT = 35; + const easyPlace = new Rule({ identifier: 'easyPlace', - description: { text: 'Places a structure with ease.' }, + description: { text: 'Simplifies placing blocks in a structure (arrow in bottom right inventory slot).' }, onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } }) @@ -15,13 +17,21 @@ extension.addRule(easyPlace); function onPlayerPlaceBlock(event) { const { player, block } = event; - if (!player || !block) return; + if (!player || !block || !hasArrowInCorrectSlot(player)) return; const structureBlock = fetchStructureBlock(block.location); if (!structureBlock) return; tryPlaceBlock(event, player, block, structureBlock); } +function hasArrowInCorrectSlot(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'; +} + function fetchStructureBlock(location) { const locatedStructures = structureCollection.getStructuresAtLocation(location); if (locatedStructures.length === 0) @@ -77,7 +87,7 @@ function tryConvertToDefaultState(structureBlock) { function tryPlaceBlockSurvival(event, player, block, structureBlock) { const placeableItemStack = getPlaceableItemStack(structureBlock); - console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); + // console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); if (itemSlotToUse) { event.cancel = true; diff --git a/scripts/rules/fastEasyPlace.js b/scripts/rules/fastEasyPlace.js new file mode 100644 index 0000000..c7c15de --- /dev/null +++ b/scripts/rules/fastEasyPlace.js @@ -0,0 +1,140 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server'; +import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, + blockIdToItemStackMap } from '../data'; +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.' }, + onEnableCallback: () => { runner = system.runInterval(onTick, 2); }, + onDisableCallback: () => { system.clearRun(runner); } +}) +extension.addRule(easyPlace); + +function onTick() { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + processEasyPlace(player); + } +} + +function processEasyPlace(player) { + if (!player || !isHoldingArrow(player)) return; + const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true }); + if (!structureBlock) + return; + const worldBlock = player.dimension.getBlock(structureBlock.location); + tryPlaceBlock(player, worldBlock, structureBlock.permutation); +} + +function isHoldingArrow(player) { + const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand); + if (!mainhandItemStack) + return false; + return mainhandItemStack.typeId === 'minecraft:arrow'; +} + +function tryPlaceBlock(player, worldBlock, structureBlock) { + if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(worldBlock)) return; + structureBlock = tryConvertBannedToValidBlock(structureBlock); + if (player.getGameMode() === GameMode.creative) { + placeBlock(worldBlock, structureBlock); + } else if (player.getGameMode() === GameMode.survival) { + structureBlock = tryConvertToDefaultState(structureBlock); + tryPlaceBlockSurvival(player, worldBlock, structureBlock); + } +} + +function locationIsPlaceable(worldBlock) { + return worldBlock.isAir; +} + +function isBannedBlock(player, structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (bannedBlocks.includes(blockId)) + return true; + if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId)) + return true; + const allowedStates = whitelistedBlockStates[blockId]; + if (allowedStates) { + for (const [stateKey, stateValue] of Object.entries(allowedStates)) { + if (structureBlock.getState(stateKey) !== stateValue) + return true; + } + } + return false; +} + +function tryConvertBannedToValidBlock(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (Object.keys(bannedToValidBlockMap).includes(blockId)) + return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates()); + return structureBlock; +} + +function tryConvertToDefaultState(structureBlock) { + const newStates = {}; + for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) { + if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey]) + newStates[stateKey] = resetToBlockStates[stateKey]; + else + newStates[stateKey] = stateValue; + } + return BlockPermutation.resolve(structureBlock.type.id, newStates); +} + +function tryPlaceBlockSurvival(player, block, structureBlock) { + const placeableItemStack = getPlaceableItemStack(structureBlock); + // console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); + const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); + if (itemSlotToUse) { + placeBlock(block, structureBlock, itemSlotToUse); + } +} + +function getPlaceableItemStack(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + const newItemId = blockIdToItemStackMap[blockId]; + return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack(); +} + +function fetchMatchingItemSlot(player, itemToMatchId) { + if (!itemToMatchId) + return void 0; + const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; + if (!inventory) + return void 0; + for (let index = 0; index < inventory.size; index++) { + const itemSlot = inventory.getSlot(index); + if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatchId) + return itemSlot; + } +} + +function placeBlock(block, structureBlock, itemSlot) { + system.run(() => { + if (itemSlot) { + consumeItem(itemSlot); + } + block.setPermutation(structureBlock); + }); +} + +function consumeItem(itemSlot) { + if (specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]) { + consumeSpecial(itemSlot); + } else { + if (itemSlot.amount === 1) + itemSlot.setItem(void 0); + else + itemSlot.amount--; + } +} + +function consumeSpecial(itemSlot) { + itemSlot.setItem(new ItemStack(specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')])); +} \ No newline at end of file diff --git a/scripts/rules/showStructBlockInfo.js b/scripts/rules/showStructBlockInfo.js new file mode 100644 index 0000000..208ce2a --- /dev/null +++ b/scripts/rules/showStructBlockInfo.js @@ -0,0 +1,35 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { system, world } from '@minecraft/server'; +import { Raycaster } from '../classes/Raycaster'; + +let runner = void 0; +const showStructBlockInfo = new Rule({ + identifier: 'showStructBlockInfo', + description: { text: 'Shows block and state info for structure blocks you are looking at.' }, + onEnableCallback: () => { runner = system.runInterval(onTick); }, + onDisableCallback: () => { system.clearRun(runner); } +}) +extension.addRule(showStructBlockInfo); + +function onTick() { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + showStructureBlockInfo(player); + } +} + +function showStructureBlockInfo(player) { + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + if (!block) + return; + player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); +} + +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 From 9eea8b441386deaacddda04fc516b59d69d8f930 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 17 Mar 2025 00:52:43 -0700 Subject: [PATCH 4/4] make showStructBlockInfo not a rule --- README.md | 12 +++++----- scripts/classes/BlockInfo.js | 24 +++++++++++++++++++ scripts/main.js | 4 +++- scripts/rules/showStructBlockInfo.js | 35 ---------------------------- 4 files changed, 33 insertions(+), 42 deletions(-) create mode 100644 scripts/classes/BlockInfo.js diff --git a/README.md b/README.md index 1573aa2..e467fef 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ Removes a structure. ## Roadmap -- [ ] Add new structures by pasting a string -- [ ] Make a viewable material list +- [ ] place structure **(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 -- [ ] Make automatic material grabbing from inventories -- [ ] structure placement (ghost and solid) -- [ ] easyPlace rule -- [ ] more litematica features! - [ ] send structure as a scriptevent so that other addons can use it +- [ ] more litematica features! ## Issues & Suggestions diff --git a/scripts/classes/BlockInfo.js b/scripts/classes/BlockInfo.js new file mode 100644 index 0000000..5d6c30c --- /dev/null +++ b/scripts/classes/BlockInfo.js @@ -0,0 +1,24 @@ +import { system, world } from '@minecraft/server'; +import { Raycaster } from '../classes/Raycaster'; + +system.runInterval(() => { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + showStructureBlockInfo(player); + } +}); + +function showStructureBlockInfo(player) { + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + if (!block) + return; + player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); +} + +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 diff --git a/scripts/main.js b/scripts/main.js index da757bb..9b6d634 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,7 +1,9 @@ // Rules import './rules/easyPlace'; import './rules/fastEasyPlace'; -import './rules/showStructBlockInfo'; // Commands import './commands/struct'; + +// Other +import './classes/BlockInfo'; diff --git a/scripts/rules/showStructBlockInfo.js b/scripts/rules/showStructBlockInfo.js index 208ce2a..e69de29 100644 --- a/scripts/rules/showStructBlockInfo.js +++ b/scripts/rules/showStructBlockInfo.js @@ -1,35 +0,0 @@ -import { Rule } from '../lib/canopy/CanopyExtension'; -import { extension } from '../config'; -import { system, world } from '@minecraft/server'; -import { Raycaster } from '../classes/Raycaster'; - -let runner = void 0; -const showStructBlockInfo = new Rule({ - identifier: 'showStructBlockInfo', - description: { text: 'Shows block and state info for structure blocks you are looking at.' }, - onEnableCallback: () => { runner = system.runInterval(onTick); }, - onDisableCallback: () => { system.clearRun(runner); } -}) -extension.addRule(showStructBlockInfo); - -function onTick() { - for (const player of world.getAllPlayers()) { - if (!player) - continue; - showStructureBlockInfo(player); - } -} - -function showStructureBlockInfo(player) { - const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); - if (!block) - return; - player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); -} - -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