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/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/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..c558a04 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,18 @@ class StructureCollection { struct.remove(); delete this.#structures[name]; } + + 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 d4cf73e..b3664d0 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -1,13 +1,12 @@ 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({ name: 'struct', description: { text: 'Manages current StrucTool structures.' }, - usage: 'struct', + usage: 'struct [args...]', callback: structCommand, args: [ { type: 'string', name: 'name' }, @@ -41,7 +40,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 +72,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..ccb7b80 --- /dev/null +++ b/scripts/data.js @@ -0,0 +1,52 @@ +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', + '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', + 'lit_redstone_lamp': 'redstone_lamp', + 'unlit_redstone_torch': 'redstone_torch' +}; + +export const resetToBlockStates = { + 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: 0, + cluster_count: 0 +}; + +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/main.js b/scripts/main.js index f03bc75..9b6d634 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,4 +1,9 @@ // Rules +import './rules/easyPlace'; +import './rules/fastEasyPlace'; // Commands -import './commands/struct.js'; +import './commands/struct'; + +// Other +import './classes/BlockInfo'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js new file mode 100644 index 0000000..3a983da --- /dev/null +++ b/scripts/rules/easyPlace.js @@ -0,0 +1,139 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { BlockPermutation, EntityComponentTypes, GameMode, ItemStack, system, world } from '@minecraft/server'; +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: 'Simplifies placing blocks in a structure (arrow in bottom right inventory slot).' }, + 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 || !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) + 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); + 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 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 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:', '')])); +} 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..e69de29