From 211e0820274e4d612df5b262d84f0cba44080cce Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 9 May 2026 14:10:28 -0600 Subject: [PATCH 1/6] Fixed wand and added /stack command --- src/endstone_worldedit/commands/stack.py | 116 +++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/endstone_worldedit/commands/stack.py diff --git a/src/endstone_worldedit/commands/stack.py b/src/endstone_worldedit/commands/stack.py new file mode 100644 index 0000000..831de6d --- /dev/null +++ b/src/endstone_worldedit/commands/stack.py @@ -0,0 +1,116 @@ +from endstone_worldedit.utils import command_executor +from endstone.block import BlockFace + +command = { + "stack": { + "description": "Clone the selected area to the direction you are looking.", + "usages": ["/stack"], + "permissions": ["worldedit.command.stack"] + } +} + +@command_executor("stack", selection_required=True) +def handler(plugin, sender, args): + # TODO: Add support for stacking + player_uuid = sender.unique_id + pos1 = plugin.selections[player_uuid]['pos1'] + pos2 = plugin.selections[player_uuid]['pos2'] + + dimension = sender.dimension + + yaw = sender.location.yaw % 360 + + if 315 <= yaw or yaw < 45: + direction = BlockFace.SOUTH + elif 45 <= yaw < 135: + direction = BlockFace.WEST + elif 135 <= yaw < 225: + direction = BlockFace.NORTH + elif 225 <= yaw < 315: + direction = BlockFace.EAST + else: + direction = BlockFace.SOUTH + + undo_entry = [] + plugin.redo_history[player_uuid] = [] + + original_blocks = [] + blocks_to_change = [] + + min_x, max_x = min(pos1[0], pos2[0]), max(pos1[0], pos2[0]) + min_y, max_y = min(pos1[1], pos2[1]), max(pos1[1], pos2[1]) + min_z, max_z = min(pos1[2], pos2[2]), max(pos1[2], pos2[2]) + + width = max_x - min_x + 1 + length = max_z - min_z + 1 + + for x in range(int(min_x), int(max_x) + 1): + for y in range(int(min_y), int(max_y) + 1): + for z in range(int(min_z), int(max_z) + 1): + block = dimension.get_block_at(x, y, z) + original_blocks.append((x, y, z, block.type, block.data)) + + if direction == BlockFace.NORTH: + offset = (0, 0, -length) + + elif direction == BlockFace.SOUTH: + offset = (0, 0, length) + + elif direction == BlockFace.WEST: + offset = (-width, 0, 0) + + elif direction == BlockFace.EAST: + offset = (width, 0, 0) + + else: + sender.send_message("Unsupported direction.") + return False + + for x, y, z, block_type, block_data in original_blocks: + new_x = x + offset[0] + new_y = y + offset[1] + new_z = z + offset[2] + + blocks_to_change.append(( + new_x, + new_y, + new_z, + block_type, + block_data + )) + + affected_blocks = len(blocks_to_change) + + for x, y, z, _, _ in blocks_to_change: + block = dimension.get_block_at(x, y, z) + undo_entry.append((x, y, z, block.type, block.data)) + + if player_uuid not in plugin.undo_history: + plugin.undo_history[player_uuid] = [] + + plugin.undo_history[player_uuid].append(undo_entry) + + if affected_blocks > plugin.plugin_config["async-threshold"]: + plugin.tasks[player_uuid] = { + "dimension": dimension, + "blocks": blocks_to_change + } + + sender.send_message( + f"Starting async operation for {affected_blocks} blocks..." + ) + + else: + for x, y, z, block_type, block_data in blocks_to_change: + block = dimension.get_block_at(x, y, z) + + block.set_type(block_type) + + if block_data is not None: + block.set_data(block_data) + + sender.send_message( + f"Operation complete ({affected_blocks} blocks affected)." + ) + + return True \ No newline at end of file From 7c1da9835b8c6a40550d2c9090489eaa2925bad6 Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 9 May 2026 14:11:16 -0600 Subject: [PATCH 2/6] Fixed wand to avoid vanilla wooden axe --- src/endstone_worldedit/commands/wand.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/endstone_worldedit/commands/wand.py b/src/endstone_worldedit/commands/wand.py index 44ffdbb..0f6a0df 100644 --- a/src/endstone_worldedit/commands/wand.py +++ b/src/endstone_worldedit/commands/wand.py @@ -1,5 +1,6 @@ from endstone.inventory import ItemStack from endstone_worldedit.utils import command_executor +from endstone.nbt import StringTag, CompoundTag command = { "wand": { @@ -12,6 +13,15 @@ @command_executor("wand") def handler(plugin, sender, args): - sender.inventory.add_item(ItemStack("minecraft:wooden_axe")) - sender.send_message("You have been given the wand tool.") - return True + wand_item = ItemStack("minecraft:wooden_axe") + # Fixed wand item, using NBT to enfoce the item to be a wand tool and prevent using normal wooden axe as a wand tool + meta = wand_item.item_meta + meta.display_name = "§bWand Tool" + meta.lore = ["§7Left-click to set pos1", "§7Right-click to set pos2"] + wand_item.set_item_meta(meta) + nbt = CompoundTag({"worldedit": StringTag("wand")}) + wand_item.nbt = nbt + plugin.logger.error(str(wand_item.nbt)) + sender.inventory.add_item(wand_item) + sender.send_message("You have been given a wand tool.") + return True \ No newline at end of file From 89b05682b2c83629b734186065cc185c9c402eaa Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 9 May 2026 14:11:54 -0600 Subject: [PATCH 3/6] Fixed wand verification --- src/endstone_worldedit/plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/endstone_worldedit/plugin.py b/src/endstone_worldedit/plugin.py index 7f56f1b..7eb46a4 100644 --- a/src/endstone_worldedit/plugin.py +++ b/src/endstone_worldedit/plugin.py @@ -13,7 +13,7 @@ class WorldEditPlugin(Plugin): - api_version = "0.10" + api_version = "0.11" commands = preloaded_commands def __init__(self): @@ -35,6 +35,8 @@ def on_load(self): schematic_path = self.plugin_config.get("schematic-path", "plugins/WorldEdit/schematics") if not os.path.exists(schematic_path): os.makedirs(schematic_path) + + # TODO: Implement mcstructures load def load_config(self): config_path = "plugins/WorldEdit/config.json" @@ -105,6 +107,7 @@ def load_config(self): "particle-type": "minecraft:endrod", "particle-density-step": 5, "schematic-path": "plugins/WorldEdit/schematics", + "mcstructures-path": "plugins/WorldEdit/mcstructures", "block_translation_map": default_block_translation_map } @@ -214,12 +217,12 @@ def on_command(self, sender: CommandSender, command: Command, args: list[str]) - handler = self.handlers[command.name] return handler(self, sender, args) return False - + @event_handler(priority=EventPriority.HIGH) def on_block_break(self, event: BlockBreakEvent): player = event.player item = player.inventory.item_in_main_hand - if item is not None and item.type == "minecraft:wooden_axe": + if item and item.nbt and "worldedit" in item.nbt and item.nbt["worldedit"].value == "wand": event.cancel() player_uuid = player.unique_id if player_uuid not in self.selections: @@ -240,7 +243,7 @@ def on_player_interact(self, event: PlayerInteractEvent): if event.action.name == "RIGHT_CLICK_BLOCK": item = player.inventory.item_in_main_hand - if item is not None and item.type == "minecraft:wooden_axe": + if item and item.nbt and "worldedit" in item.nbt and item.nbt["worldedit"].value == "wand": self.interaction_cooldown[player_uuid] = current_time if player_uuid not in self.selections: self.selections[player_uuid] = {} From a398c5469b004b3e14b89f0cb67971cf9746da36 Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 16 May 2026 19:41:59 -0600 Subject: [PATCH 4/6] Added Brushes / Added mcstructure loading --- src/endstone_worldedit/commands/brush.py | 272 +++++++++++++++++++++++ src/endstone_worldedit/commands/mask.py | 71 ++++++ src/endstone_worldedit/commands/mcs.py | 178 +++++++++++++++ src/endstone_worldedit/commands/stack.py | 78 +++---- src/endstone_worldedit/commands/wand.py | 7 +- src/endstone_worldedit/plugin.py | 41 ++-- 6 files changed, 580 insertions(+), 67 deletions(-) create mode 100644 src/endstone_worldedit/commands/brush.py create mode 100644 src/endstone_worldedit/commands/mask.py create mode 100644 src/endstone_worldedit/commands/mcs.py diff --git a/src/endstone_worldedit/commands/brush.py b/src/endstone_worldedit/commands/brush.py new file mode 100644 index 0000000..b8a0be3 --- /dev/null +++ b/src/endstone_worldedit/commands/brush.py @@ -0,0 +1,272 @@ +from endstone.inventory import ItemStack +from endstone.nbt import StringTag, CompoundTag, IntTag +from endstone_worldedit.utils import command_executor +import math + +command = { + "brush": { + "description": "Binds a brush to the current item player is holding.", + "usages": [ + "/brush sphere [-h]", + "/brush cylinder [-h]", + "/brush cube [-h]", + "/brush none" + ], + "permissions": ["worldedit.command.brush"] + } +} + +@command_executor("brush") +def handler(plugin, sender, args): + if len(args) < 1: + sender.send_message("Usage: /brush [args...]") + return False + + brush_type = args[0].lower() + if brush_type == "none": + item = sender.inventory.item_in_main_hand + if item and item.nbt and "worldedit" in item.nbt and item.nbt["worldedit"].value == "brush": + item.nbt = CompoundTag() + sender.send_message("Brush unbound from current item.") + else: + sender.send_message("No brush bound to current item.") + return True + + if brush_type not in ("sphere", "cylinder", "cube"): + sender.send_message("Invalid brush type. Use: sphere, cylinder, cube, or none.") + return False + + hollow = False + filtered_args = [] + for arg in args[1:]: + if arg == "-h": + hollow = True + else: + filtered_args.append(arg) + + if brush_type == "sphere": + if len(filtered_args) < 2: + sender.send_message("Usage: /brush sphere [-h]") + return False + try: + radius = int(filtered_args[0]) + except ValueError: + sender.send_message("Radius must be an integer.") + return False + if radius < 1: + sender.send_message("Radius must be a positive integer.") + return False + block_type = filtered_args[1] + _bind_brush_to_item(sender, brush_type, radius, block_type, hollow) + + elif brush_type == "cylinder": + if len(filtered_args) < 3: + sender.send_message("Usage: /brush cylinder [-h]") + return False + try: + radius = int(filtered_args[0]) + height = int(filtered_args[1]) + except ValueError: + sender.send_message("Radius and height must be integers.") + return False + if radius < 1 or height < 1: + sender.send_message("Radius and height must be positive integers.") + return False + block_type = filtered_args[2] + _bind_brush_to_item(sender, brush_type, radius, block_type, hollow, height) + + elif brush_type == "cube": + if len(filtered_args) < 2: + sender.send_message("Usage: /brush cube [-h]") + return False + try: + radius = int(filtered_args[0]) + except ValueError: + sender.send_message("Radius must be an integer.") + return False + if radius < 1: + sender.send_message("Radius must be a positive integer.") + return False + block_type = filtered_args[1] + _bind_brush_to_item(sender, brush_type, radius, block_type, hollow) + + return True + +def _bind_brush_to_item(sender, brush_type, radius, block_type, hollow, height=None): + item = sender.inventory.item_in_main_hand + if not item or item.type == "minecraft:air": + sender.send_message("You must be holding an item.") + return False + + brush_compound = CompoundTag({ + "type": StringTag(brush_type), + "radius": IntTag(radius), + "block": StringTag(block_type), + "hollow": StringTag("true" if hollow else "false"), + "mask": StringTag("none") + }) + if height is not None: + brush_compound["height"] = IntTag(height) + + nbt = CompoundTag({ + "worldedit": StringTag("brush"), + "brush": brush_compound + }) + + item.nbt = nbt + + meta = item.item_meta + meta.display_name = f"§bBrush ({brush_type})" + lore = [ + f"§7Type: {brush_type}", + f"§7Radius: {radius}", + f"§7Block: {block_type}", + f"§7Hollow: {hollow}", + f"§7Mask: none" + ] + if height is not None: + lore.insert(2, f"§7Height: {height}") + meta.lore = lore + item.set_item_meta(meta) + slot = sender.inventory.held_item_slot + sender.inventory.set_item(slot, None) + sender.inventory.set_item(slot, item) + + msg = f"{brush_type.capitalize()} brush bound to current item (radius: {radius}, block: {block_type}, hollow: {hollow})" + if height is not None: + msg = f"{brush_type.capitalize()} brush bound to current item (radius: {radius}, height: {height}, block: {block_type}, hollow: {hollow})" + sender.send_message(msg) + return True + +def execute_brush(plugin, player, block, brush_data): + brush_type = brush_data["type"].value + radius = brush_data["radius"].value + block_type = brush_data["block"].value + hollow = brush_data["hollow"].value == "true" + mask = None + if "mask" in brush_data and brush_data["mask"].value != "none": + mask = brush_data["mask"].value + if "height" in brush_data: + height = brush_data["height"].value + else: + height = 1 + + player_uuid = player.unique_id + dimension = player.dimension + cx, cy, cz = block.x, block.y, block.z + blocks_to_change = [] + if brush_type == "sphere": + for x in range(cx - radius, cx + radius + 1): + for y in range(cy - radius, cy + radius + 1): + for z in range(cz - radius, cz + radius + 1): + distance = math.sqrt((x - cx) ** 2 + (y - cy) ** 2 + (z - cz) ** 2) + if hollow: + if not (radius - 1 < distance <= radius): + continue + elif distance > radius: + continue + + if mask: + try: + existing = dimension.get_block_at(x, y, z) + if str(existing.type) != mask: + continue + except RuntimeError: + continue + + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + + elif brush_type == "cylinder": + for y in range(cy, cy + height): + for x in range(cx - radius, cx + radius + 1): + for z in range(cz - radius, cz + radius + 1): + distance = math.sqrt((x - cx) ** 2 + (z - cz) ** 2) + if hollow: + if not (radius - 1 < distance <= radius): + continue + elif distance > radius: + continue + + if mask: + try: + existing = dimension.get_block_at(x, y, z) + if existing.type != mask: + continue + except RuntimeError: + continue + + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + + elif brush_type == "cube": + for x in range(cx - radius, cx + radius + 1): + for y in range(cy - radius, cy + radius + 1): + for z in range(cz - radius, cz + radius + 1): + if hollow: + is_surface = (x == cx - radius or x == cx + radius or + y == cy - radius or y == cy + radius or + z == cz - radius or z == cz + radius) + if not is_surface: + continue + + if mask: + try: + existing = dimension.get_block_at(x, y, z) + if existing.type != mask: + continue + except RuntimeError: + continue + + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + + if not blocks_to_change: + player.send_message("No blocks to change.") + return + + undo_entry = [] + for x, y, z, _, _ in blocks_to_change: + try: + b = dimension.get_block_at(x, y, z) + undo_entry.append((x, y, z, b.type, b.data)) + except RuntimeError: + continue + + if player_uuid not in plugin.undo_history: + plugin.undo_history[player_uuid] = [] + plugin.undo_history[player_uuid].append(undo_entry) + plugin.redo_history[player_uuid] = [] + if len(blocks_to_change) > plugin.plugin_config["async-threshold"]: + plugin.tasks[player_uuid] = {"dimension": dimension, "blocks": blocks_to_change} + player.send_message(f"Placing {len(blocks_to_change)} blocks asynchronously...") + else: + for x, y, z, bt, _ in blocks_to_change: + try: + b = dimension.get_block_at(x, y, z) + b.set_type(bt) + except RuntimeError: + continue + player.send_message(f"Brush placed {len(blocks_to_change)} blocks.") + +def _get_target_block(player, max_distance=100): + """Raycast from player's eyes to find the first solid block they're looking at.""" + loc = player.location + direction = loc.direction + dimension = player.dimension + x, y, z = loc.x, loc.y + 1.62, loc.z + dx = direction.x + dy = direction.y + dz = direction.z + step = 0.5 + for i in range(int(max_distance / step)): + x += dx * step + y += dy * step + z += dz * step + block_x = int(math.floor(x)) + block_y = int(math.floor(y)) + block_z = int(math.floor(z)) + try: + block = dimension.get_block_at(block_x, block_y, block_z) + if block.type != "minecraft:air": + return block + except RuntimeError: + return None + return None \ No newline at end of file diff --git a/src/endstone_worldedit/commands/mask.py b/src/endstone_worldedit/commands/mask.py new file mode 100644 index 0000000..869a09e --- /dev/null +++ b/src/endstone_worldedit/commands/mask.py @@ -0,0 +1,71 @@ +from endstone.nbt import StringTag, CompoundTag +from endstone_worldedit.utils import command_executor + +command = { + "mask": { + "description": "Sets a mask for the current brush, replacing only the specified block.", + "usages": ["/mask ", "/mask none"], + "permissions": ["worldedit.command.mask"] + } +} + +@command_executor("mask") +def handler(plugin, sender, args): + item = sender.inventory.item_in_main_hand + if not item or not item.nbt or "worldedit" not in item.nbt or item.nbt["worldedit"].value != "brush": + sender.send_message("You must be holding a brush item.") + return False + + if len(args) < 1: + sender.send_message("Usage: /mask or /mask none") + return False + + mask_type = args[0].lower() + brush_data = item.nbt["brush"] + if mask_type == "none": + new_mask = StringTag("none") + lore_mask = "none" + msg = "Mask removed from current brush." + else: + new_mask = StringTag(f"minecraft:{mask_type}") + lore_mask = mask_type + msg = f"Brush mask set to {mask_type}." + + new_brush = CompoundTag({ + "type": brush_data["type"], + "radius": brush_data["radius"], + "block": brush_data["block"], + "hollow": brush_data["hollow"], + "mask": new_mask + }) + if "height" in brush_data: + new_brush["height"] = brush_data["height"] + + height_str = "" + if "height" in brush_data: + height_str = str(brush_data["height"].value) + + slot = sender.inventory.held_item_slot + sender.inventory.set_item(slot, None) + + item.nbt = CompoundTag({ + "worldedit": StringTag("brush"), + "brush": new_brush + }) + + meta = item.item_meta + meta.display_name = f"§bBrush ({brush_data['type'].value})" + lore = [ + f"§7Type: {brush_data['type'].value}", + f"§7Radius: {brush_data['radius'].value}", + f"§7Block: {brush_data['block'].value}", + f"§7Hollow: {brush_data['hollow'].value}", + f"§7Mask: {lore_mask}" + ] + if height_str: + lore.insert(2, f"§7Height: {height_str}") + meta.lore = lore + item.set_item_meta(meta) + sender.inventory.set_item(slot, item) + sender.send_message(msg) + return True \ No newline at end of file diff --git a/src/endstone_worldedit/commands/mcs.py b/src/endstone_worldedit/commands/mcs.py new file mode 100644 index 0000000..d1977c1 --- /dev/null +++ b/src/endstone_worldedit/commands/mcs.py @@ -0,0 +1,178 @@ +import os +import nbtlib +import numpy as np +from endstone_worldedit.utils import command_executor + +class MCStructureReader: + def __init__(self, file_path): + self.nbt_file = nbtlib.load(file_path, byteorder='little') + if "" in self.nbt_file.keys(): + self.nbt_file = self.nbt_file[""] + self.blocks_raw = np.array(list(map(int, self.nbt_file["structure"]["block_indices"][0]))) + self.size = np.array(list(map(int, self.nbt_file["size"]))) + self.palette = self.nbt_file["structure"]["palette"]["default"]["block_palette"] + self.origin = np.array(list(map(int, self.nbt_file["structure_world_origin"]))) + self._process_blockmap() + + def _process_blockmap(self): + index_of_air = 0 + for i in range(len(self.palette)): + if self.palette[i]["name"] == "minecraft:air": + index_of_air = i + break + self.cube = self.blocks_raw.copy() + self.cube = self.cube + 1 + self.palette = [{"name": "minecraft:air", "states": {}}] + list(self.palette) + self.cube[self.cube == index_of_air + 1] = 0 + width, height, length = int(self.size[0]), int(self.size[1]), int(self.size[2]) + self.cube = self.cube.reshape((width, height, length)) + + def get_block(self, x, y, z): + if x < 0 or y < 0 or z < 0: + return None + if x >= self.size[0] or y >= self.size[1] or z >= self.size[2]: + return None + palette_index = int(self.cube[x, y, z]) + if palette_index < 0 or palette_index >= len(self.palette): + return None + return self.palette[palette_index] + + def get_size(self): + return self.size + +command = { + "mcs": { + "description": "Loads a .mcstructure file.", + "usages": [ + "/mcs load ", + "/mcs list" + ], + "permissions": ["worldedit.command.mcstructure"] + } +} + +@command_executor("mcs") +def handler(plugin, sender, args): + if len(args) < 1: + sender.send_message("Usage: /mcs [name]") + return False + + sub_command = args[0].lower() + mcstructures_path = plugin.plugin_config.get("mcstructures-path", "plugins/WorldEdit/mcstructures") + + if sub_command == "list": + if not os.path.exists(mcstructures_path): + sender.send_message("MCStructures directory not found.") + return False + files = [f.replace('.mcstructure', '') for f in os.listdir(mcstructures_path) if f.endswith('.mcstructure')] + if not files: + sender.send_message("No structures found.") + else: + sender.send_message("Available structures: " + ", ".join(files)) + return True + + if sub_command == "load": + if len(args) < 2: + sender.send_message("Usage: /mcs load ") + return False + + name = args[1] + file_path = os.path.join(mcstructures_path, f"{name}.mcstructure") + + if not os.path.exists(file_path): + sender.send_message(f"Structure '{name}.mcstructure' not found.") + return False + + try: + reader = MCStructureReader(file_path) + except Exception as e: + sender.send_message(f"Error reading structure file: {e}") + return False + + size = reader.get_size() + width, height, length = int(size[0]), int(size[1]), int(size[2]) + + player_uuid = sender.unique_id + dimension = sender.dimension + player_location = sender.location + + sender.send_message(f"Loading structure ({width}×{height}×{length})...") + + blocks_to_change = [] + for y in range(height): + for z in range(length): + for x in range(width): + block_info = reader.get_block(x, y, z) + if block_info is None: + continue + + block_name = str(block_info['name']) + if block_name in ("minecraft:air", "minecraft:flowing_water", "minecraft:flowing_lava"): + continue + + target_x = int(player_location.x) + x + target_y = int(player_location.y) + y + target_z = int(player_location.z) + z + + states = {} + if 'states' in block_info: + for state_key, state_val in block_info['states'].items(): + try: + states[state_key] = int(state_val) + except (TypeError, ValueError): + states[state_key] = str(state_val) + + blocks_to_change.append((target_x, target_y, target_z, block_name, states)) + + if not blocks_to_change: + sender.send_message("Structure is empty or only contains air.") + return True + + sender.send_message(f"Placing {len(blocks_to_change)} blocks...") + + dependent_blocks = [ + "flower", "sapling", "mushroom", "torch", "rail", "redstone_wire", "repeater", "comparator", + "sign", "door", "lever", "button", "pressure_plate", "tripwire_hook", "tripwire", "banner" + ] + + solid_pass = [b for b in blocks_to_change if not any(d in b[3] for d in dependent_blocks)] + dependent_pass = [b for b in blocks_to_change if any(d in b[3] for d in dependent_blocks)] + + full_undo_entry = [] + for x, y, z, _, _ in blocks_to_change: + try: + old = dimension.get_block_at(x, y, z) + full_undo_entry.append((x, y, z, str(old.type), old.data)) + except RuntimeError: + continue + + if player_uuid not in plugin.undo_history: + plugin.undo_history[player_uuid] = [] + plugin.undo_history[player_uuid].append(full_undo_entry) + plugin.redo_history[player_uuid] = [] + + def execute_pass(blocks_pass): + for x, y, z, block_type, states in blocks_pass: + try: + block = dimension.get_block_at(x, y, z) + block.set_type(block_type) + if states: + state_str = ",".join(f'"{k}":{v}' for k, v in states.items()) + command = f"setblock {x} {y} {z} {block_type} [{state_str}]" + plugin.server.dispatch_command(plugin.silent_sender, command) + except RuntimeError as e: + plugin.logger.error(f"Skipping block '{block_type}': {e}") + continue + + if len(blocks_to_change) > plugin.plugin_config["async-threshold"]: + execute_pass(solid_pass) + execute_pass(dependent_pass) + else: + execute_pass(solid_pass) + execute_pass(dependent_pass) + + sender.send_message(f"Operation complete ({len(blocks_to_change)} blocks affected).") + return True + + sender.send_message(f"Unknown sub-command '{sub_command}'. Use load or list.") + return False \ No newline at end of file diff --git a/src/endstone_worldedit/commands/stack.py b/src/endstone_worldedit/commands/stack.py index 831de6d..26225e9 100644 --- a/src/endstone_worldedit/commands/stack.py +++ b/src/endstone_worldedit/commands/stack.py @@ -4,22 +4,28 @@ command = { "stack": { "description": "Clone the selected area to the direction you are looking.", - "usages": ["/stack"], + "usages": ["/stack [count: int]"], "permissions": ["worldedit.command.stack"] } } @command_executor("stack", selection_required=True) def handler(plugin, sender, args): - # TODO: Add support for stacking player_uuid = sender.unique_id pos1 = plugin.selections[player_uuid]['pos1'] pos2 = plugin.selections[player_uuid]['pos2'] - dimension = sender.dimension - + count = 1 + if args: + try: + count = int(args[0]) + if count < 1: + sender.send_message("Count must be a positive integer.") + return False + except ValueError: + sender.send_message("Invalid count. Usage: /stack [count]") + return False yaw = sender.location.yaw % 360 - if 315 <= yaw or yaw < 45: direction = BlockFace.SOUTH elif 45 <= yaw < 135: @@ -30,20 +36,15 @@ def handler(plugin, sender, args): direction = BlockFace.EAST else: direction = BlockFace.SOUTH - undo_entry = [] plugin.redo_history[player_uuid] = [] - - original_blocks = [] - blocks_to_change = [] - min_x, max_x = min(pos1[0], pos2[0]), max(pos1[0], pos2[0]) min_y, max_y = min(pos1[1], pos2[1]), max(pos1[1], pos2[1]) min_z, max_z = min(pos1[2], pos2[2]), max(pos1[2], pos2[2]) - width = max_x - min_x + 1 length = max_z - min_z + 1 - + height = max_y - min_y + 1 + original_blocks = [] for x in range(int(min_x), int(max_x) + 1): for y in range(int(min_y), int(max_y) + 1): for z in range(int(min_z), int(max_z) + 1): @@ -52,65 +53,44 @@ def handler(plugin, sender, args): if direction == BlockFace.NORTH: offset = (0, 0, -length) - elif direction == BlockFace.SOUTH: offset = (0, 0, length) - elif direction == BlockFace.WEST: offset = (-width, 0, 0) - elif direction == BlockFace.EAST: offset = (width, 0, 0) - else: sender.send_message("Unsupported direction.") return False - - for x, y, z, block_type, block_data in original_blocks: - new_x = x + offset[0] - new_y = y + offset[1] - new_z = z + offset[2] - - blocks_to_change.append(( - new_x, - new_y, - new_z, - block_type, - block_data - )) - + blocks_to_change = [] + for i in range(1, count + 1): + for x, y, z, block_type, block_data in original_blocks: + new_x = x + offset[0] * i + new_y = y + offset[1] * i + new_z = z + offset[2] * i + blocks_to_change.append((new_x, new_y, new_z, block_type, block_data)) + for new_x, new_y, new_z, _, _ in blocks_to_change: + try: + block = dimension.get_block_at(new_x, new_y, new_z) + undo_entry.append((new_x, new_y, new_z, block.type, block.data)) + except RuntimeError: + sender.send_message(f"§cStack would place blocks out of bounds. Aborting.§r") + return False affected_blocks = len(blocks_to_change) - - for x, y, z, _, _ in blocks_to_change: - block = dimension.get_block_at(x, y, z) - undo_entry.append((x, y, z, block.type, block.data)) - if player_uuid not in plugin.undo_history: plugin.undo_history[player_uuid] = [] - plugin.undo_history[player_uuid].append(undo_entry) - if affected_blocks > plugin.plugin_config["async-threshold"]: plugin.tasks[player_uuid] = { "dimension": dimension, "blocks": blocks_to_change } - - sender.send_message( - f"Starting async operation for {affected_blocks} blocks..." - ) - + sender.send_message(f"Starting async operation for {affected_blocks} blocks ({count} stacks)...") else: for x, y, z, block_type, block_data in blocks_to_change: block = dimension.get_block_at(x, y, z) - block.set_type(block_type) - if block_data is not None: block.set_data(block_data) - - sender.send_message( - f"Operation complete ({affected_blocks} blocks affected)." - ) - + sender.send_message(f"Operation complete ({affected_blocks} blocks affected, {count} stacks).") return True \ No newline at end of file diff --git a/src/endstone_worldedit/commands/wand.py b/src/endstone_worldedit/commands/wand.py index 0f6a0df..a5ba213 100644 --- a/src/endstone_worldedit/commands/wand.py +++ b/src/endstone_worldedit/commands/wand.py @@ -14,14 +14,13 @@ @command_executor("wand") def handler(plugin, sender, args): wand_item = ItemStack("minecraft:wooden_axe") - # Fixed wand item, using NBT to enfoce the item to be a wand tool and prevent using normal wooden axe as a wand tool + # Fixed wand item, using NBT to enforce the item to be a wand tool and prevent using normal wooden axe as a wand tool + nbt = CompoundTag({"worldedit": StringTag("wand")}) + wand_item.nbt = nbt meta = wand_item.item_meta meta.display_name = "§bWand Tool" meta.lore = ["§7Left-click to set pos1", "§7Right-click to set pos2"] wand_item.set_item_meta(meta) - nbt = CompoundTag({"worldedit": StringTag("wand")}) - wand_item.nbt = nbt - plugin.logger.error(str(wand_item.nbt)) sender.inventory.add_item(wand_item) sender.send_message("You have been given a wand tool.") return True \ No newline at end of file diff --git a/src/endstone_worldedit/plugin.py b/src/endstone_worldedit/plugin.py index 7eb46a4..06657d5 100644 --- a/src/endstone_worldedit/plugin.py +++ b/src/endstone_worldedit/plugin.py @@ -25,18 +25,17 @@ def __init__(self): self.redo_history = {} self.clipboard = {} self.block_translation_map = {} - self.particle_toggle = {} # Stores player UUID -> bool + self.particle_toggle = {} def on_load(self): self.logger.info("WorldEditPlugin has been loaded!") self.load_config() - - # Create schematics directory if it doesn't exist schematic_path = self.plugin_config.get("schematic-path", "plugins/WorldEdit/schematics") if not os.path.exists(schematic_path): os.makedirs(schematic_path) - - # TODO: Implement mcstructures load + mcstructures_path = self.plugin_config.get("mcstructures-path", "plugins/WorldEdit/mcstructures") + if not os.path.exists(mcstructures_path): + os.makedirs(mcstructures_path) def load_config(self): config_path = "plugins/WorldEdit/config.json" @@ -132,7 +131,6 @@ def on_enable(self): def show_selection_particles(self): for player_uuid, selection in self.selections.items(): - # Check if particles are enabled for this player if not self.particle_toggle.get(player_uuid, True): continue @@ -144,11 +142,8 @@ def show_selection_particles(self): min_x, max_x = min(pos1[0], pos2[0]), max(pos1[0], pos2[0]) min_y, max_y = min(pos1[1], pos2[1]), max(pos1[1], pos2[1]) min_z, max_z = min(pos1[2], pos2[2]), max(pos1[2], pos2[2]) - - # Draw a grid of particles along the edges, executed by the player step = self.plugin_config["particle-density-step"] particle_type = self.plugin_config["particle-type"] - # Draw a grid of particles along the edges, executed by the player step = self.plugin_config["particle-density-step"] particle_type = self.plugin_config["particle-type"] player_name = player.name @@ -173,6 +168,7 @@ def run_particle_command(x, y, z): run_particle_command(min_x, max_y, z) run_particle_command(max_x, max_y, z) + def run_tasks(self): for player_uuid, task_info in list(self.tasks.items()): dimension = task_info["dimension"] @@ -211,7 +207,7 @@ def run_tasks(self): if player: player.send_message(f"§cSkipped block: {block_type} ({e})§r") continue # Skip to the next block - + def on_command(self, sender: CommandSender, command: Command, args: list[str]) -> bool: if command.name in self.handlers: handler = self.handlers[command.name] @@ -238,15 +234,32 @@ def on_player_interact(self, event: PlayerInteractEvent): current_time = time.time() last_interact_time = self.interaction_cooldown.get(player_uuid, 0) - if current_time - last_interact_time < 0.1: # 100ms cooldown + if current_time - last_interact_time < 0.1: return - if event.action.name == "RIGHT_CLICK_BLOCK": - item = player.inventory.item_in_main_hand - if item and item.nbt and "worldedit" in item.nbt and item.nbt["worldedit"].value == "wand": + item = event.item + if not item or not item.nbt or "worldedit" not in item.nbt: + return + + if item.nbt["worldedit"].value == "wand": + if event.action == PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK: self.interaction_cooldown[player_uuid] = current_time if player_uuid not in self.selections: self.selections[player_uuid] = {} block = event.block self.selections[player_uuid]["pos2"] = (block.x, block.y, block.z) player.send_message(f"Position 2 set to ({block.x}, {block.y}, {block.z}).") + + elif item.nbt["worldedit"].value == "brush": + if event.action in (PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK, PlayerInteractEvent.Action.RIGHT_CLICK_AIR): + self.interaction_cooldown[player_uuid] = current_time + if "brush" in item.nbt: + from .commands.brush import execute_brush, _get_target_block + if event.has_block: + target = event.block + else: + target = _get_target_block(player) + if target: + execute_brush(self, player, target, item.nbt["brush"]) + else: + player.send_message("§cNo target block found.§r") From 9c5e5882b8df91768c0d44eb28a86675f2e77ad9 Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 13 Jun 2026 14:47:43 -0600 Subject: [PATCH 5/6] Updated --- pyproject.toml | 3 +- src/endstone_worldedit/commands/brush.py | 34 +++-- src/endstone_worldedit/commands/mcs.py | 163 +++++++++++------------ src/endstone_worldedit/commands/schem.py | 7 +- src/endstone_worldedit/config/configs.py | 91 +++++++++++++ src/endstone_worldedit/plugin.py | 85 +----------- 6 files changed, 201 insertions(+), 182 deletions(-) create mode 100644 src/endstone_worldedit/config/configs.py diff --git a/pyproject.toml b/pyproject.toml index 98d49fe..5516a53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,8 @@ authors = [ ] dependencies = [ "endstone", - "nbtlib" + "nbtlib", + "ruamel.yaml" ] [project.entry-points."endstone"] diff --git a/src/endstone_worldedit/commands/brush.py b/src/endstone_worldedit/commands/brush.py index b8a0be3..c0ff97e 100644 --- a/src/endstone_worldedit/commands/brush.py +++ b/src/endstone_worldedit/commands/brush.py @@ -7,9 +7,9 @@ "brush": { "description": "Binds a brush to the current item player is holding.", "usages": [ - "/brush sphere [-h]", - "/brush cylinder [-h]", - "/brush cube [-h]", + "/brush sphere [-h: bool]", + "/brush cylinder [-h: bool]", + "/brush cube [-h: bool]", "/brush none" ], "permissions": ["worldedit.command.brush"] @@ -39,8 +39,10 @@ def handler(plugin, sender, args): hollow = False filtered_args = [] for arg in args[1:]: - if arg == "-h": + if arg.lower() == "true": hollow = True + elif arg.lower() == "false": + continue else: filtered_args.append(arg) @@ -98,6 +100,9 @@ def _bind_brush_to_item(sender, brush_type, radius, block_type, hollow, height=N sender.send_message("You must be holding an item.") return False + if not block_type.startswith("minecraft:"): + block_type = f"minecraft:{block_type}" + brush_compound = CompoundTag({ "type": StringTag(brush_type), "radius": IntTag(radius), @@ -165,7 +170,6 @@ def execute_brush(plugin, player, block, brush_data): continue elif distance > radius: continue - if mask: try: existing = dimension.get_block_at(x, y, z) @@ -173,8 +177,10 @@ def execute_brush(plugin, player, block, brush_data): continue except RuntimeError: continue - - blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + if block_type.startswith("minecraft:"): + blocks_to_change.append((x, y, z, block_type, None)) + else: + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) elif brush_type == "cylinder": for y in range(cy, cy + height): @@ -194,8 +200,11 @@ def execute_brush(plugin, player, block, brush_data): continue except RuntimeError: continue - - blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + + if block_type.startswith("minecraft:"): + blocks_to_change.append((x, y, z, block_type, None)) + else: + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) elif brush_type == "cube": for x in range(cx - radius, cx + radius + 1): @@ -215,8 +224,11 @@ def execute_brush(plugin, player, block, brush_data): continue except RuntimeError: continue - - blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) + + if block_type.startswith("minecraft:"): + blocks_to_change.append((x, y, z, block_type, None)) + else: + blocks_to_change.append((x, y, z, f"minecraft:{block_type}", None)) if not blocks_to_change: player.send_message("No blocks to change.") diff --git a/src/endstone_worldedit/commands/mcs.py b/src/endstone_worldedit/commands/mcs.py index d1977c1..b140522 100644 --- a/src/endstone_worldedit/commands/mcs.py +++ b/src/endstone_worldedit/commands/mcs.py @@ -1,44 +1,51 @@ import os import nbtlib -import numpy as np from endstone_worldedit.utils import command_executor -class MCStructureReader: - def __init__(self, file_path): - self.nbt_file = nbtlib.load(file_path, byteorder='little') - if "" in self.nbt_file.keys(): - self.nbt_file = self.nbt_file[""] - self.blocks_raw = np.array(list(map(int, self.nbt_file["structure"]["block_indices"][0]))) - self.size = np.array(list(map(int, self.nbt_file["size"]))) - self.palette = self.nbt_file["structure"]["palette"]["default"]["block_palette"] - self.origin = np.array(list(map(int, self.nbt_file["structure_world_origin"]))) - self._process_blockmap() - - def _process_blockmap(self): - index_of_air = 0 - for i in range(len(self.palette)): - if self.palette[i]["name"] == "minecraft:air": - index_of_air = i - break - self.cube = self.blocks_raw.copy() - self.cube = self.cube + 1 - self.palette = [{"name": "minecraft:air", "states": {}}] + list(self.palette) - self.cube[self.cube == index_of_air + 1] = 0 - width, height, length = int(self.size[0]), int(self.size[1]), int(self.size[2]) - self.cube = self.cube.reshape((width, height, length)) - - def get_block(self, x, y, z): - if x < 0 or y < 0 or z < 0: - return None - if x >= self.size[0] or y >= self.size[1] or z >= self.size[2]: - return None - palette_index = int(self.cube[x, y, z]) - if palette_index < 0 or palette_index >= len(self.palette): - return None - return self.palette[palette_index] - - def get_size(self): - return self.size +# Blocks that should not be placed due to safety or admin-only restrictions +BLOCKED_BLOCKS = { + "minecraft:command_block", + "minecraft:chain_command_block", + "minecraft:repeating_command_block", + "minecraft:structure_block", + "minecraft:structure_void", + "minecraft:allow", + "minecraft:deny", + "minecraft:border_block", + "minecraft:bedrock", + "minecraft:barrier", + "minecraft:end_portal_frame", + "minecraft:end_portal", + "minecraft:portal", + "minecraft:end_gateway", + "minecraft:reserved6", + "minecraft:jigsaw", + "minecraft:flowing_lava", +} + + +def load_mcstructure(file_path): + nbt_file = nbtlib.load(file_path, byteorder='little') + if "" in nbt_file.keys(): + nbt_file = nbt_file[""] + blocks_raw = list(map(int, nbt_file["structure"]["block_indices"][0])) + size = list(map(int, nbt_file["size"])) + palette = nbt_file["structure"]["palette"]["default"]["block_palette"] + return blocks_raw, size, palette + + +def get_block(blocks_raw, size, palette, x, y, z): + width, height, length = size + if x < 0 or y < 0 or z < 0 or x >= width or y >= height or z >= length: + return None + idx = (x * height + y) * length + z + if idx < 0 or idx >= len(blocks_raw): + return None + palette_index = blocks_raw[idx] + if palette_index < 0 or palette_index >= len(palette): + return None + return palette[palette_index] + command = { "mcs": { @@ -51,6 +58,7 @@ def get_size(self): } } + @command_executor("mcs") def handler(plugin, sender, args): if len(args) < 1: @@ -77,102 +85,91 @@ def handler(plugin, sender, args): return False name = args[1] + if ".." in name or "/" in name or "\\" in name: + sender.send_message("Invalid structure name.") + return False file_path = os.path.join(mcstructures_path, f"{name}.mcstructure") - if not os.path.exists(file_path): sender.send_message(f"Structure '{name}.mcstructure' not found.") return False try: - reader = MCStructureReader(file_path) + blocks_raw, size, palette = load_mcstructure(file_path) except Exception as e: sender.send_message(f"Error reading structure file: {e}") return False - size = reader.get_size() width, height, length = int(size[0]), int(size[1]), int(size[2]) - player_uuid = sender.unique_id dimension = sender.dimension player_location = sender.location - - sender.send_message(f"Loading structure ({width}×{height}×{length})...") + sender.send_message(f"Loading structure ({width}x{height}x{length})...") blocks_to_change = [] for y in range(height): for z in range(length): for x in range(width): - block_info = reader.get_block(x, y, z) + block_info = get_block(blocks_raw, size, palette, x, y, z) if block_info is None: continue - block_name = str(block_info['name']) - if block_name in ("minecraft:air", "minecraft:flowing_water", "minecraft:flowing_lava"): + if block_name in BLOCKED_BLOCKS: continue - target_x = int(player_location.x) + x target_y = int(player_location.y) + y target_z = int(player_location.z) + z - states = {} if 'states' in block_info: for state_key, state_val in block_info['states'].items(): - try: - states[state_key] = int(state_val) - except (TypeError, ValueError): - states[state_key] = str(state_val) - + key_str = str(state_key) + if "_bit" in key_str: + states[key_str] = bool(int(state_val)) + else: + try: + states[key_str] = int(state_val) + except (TypeError, ValueError): + states[key_str] = str(state_val) blocks_to_change.append((target_x, target_y, target_z, block_name, states)) if not blocks_to_change: sender.send_message("Structure is empty or only contains air.") return True - sender.send_message(f"Placing {len(blocks_to_change)} blocks...") - - dependent_blocks = [ - "flower", "sapling", "mushroom", "torch", "rail", "redstone_wire", "repeater", "comparator", - "sign", "door", "lever", "button", "pressure_plate", "tripwire_hook", "tripwire", "banner" - ] + affected_blocks = len(blocks_to_change) + sender.send_message(f"Placing {affected_blocks} blocks...") - solid_pass = [b for b in blocks_to_change if not any(d in b[3] for d in dependent_blocks)] - dependent_pass = [b for b in blocks_to_change if any(d in b[3] for d in dependent_blocks)] + # Store undo history first + undo_entry = [] + plugin.redo_history[player_uuid] = [] - full_undo_entry = [] for x, y, z, _, _ in blocks_to_change: try: - old = dimension.get_block_at(x, y, z) - full_undo_entry.append((x, y, z, str(old.type), old.data)) + block = dimension.get_block_at(x, y, z) + undo_entry.append((x, y, z, block.type, block.data)) except RuntimeError: continue if player_uuid not in plugin.undo_history: plugin.undo_history[player_uuid] = [] - plugin.undo_history[player_uuid].append(full_undo_entry) - plugin.redo_history[player_uuid] = [] + plugin.undo_history[player_uuid].append(undo_entry) - def execute_pass(blocks_pass): - for x, y, z, block_type, states in blocks_pass: + # Execute asynchronously if the task is large + if affected_blocks > plugin.plugin_config["async-threshold"]: + plugin.tasks[player_uuid] = {"dimension": dimension, "blocks": blocks_to_change} + sender.send_message(f"Starting async operation for {affected_blocks} blocks...") + else: + for x, y, z, block_type, states in blocks_to_change: try: block = dimension.get_block_at(x, y, z) + bd = plugin.server.create_block_data(block_type, block_states=states) block.set_type(block_type) - if states: - state_str = ",".join(f'"{k}":{v}' for k, v in states.items()) - command = f"setblock {x} {y} {z} {block_type} [{state_str}]" - plugin.server.dispatch_command(plugin.silent_sender, command) - except RuntimeError as e: - plugin.logger.error(f"Skipping block '{block_type}': {e}") + block.set_data(bd) + except Exception as e: + plugin.logger.error(f"Failed block {block_type}: {e}") continue - if len(blocks_to_change) > plugin.plugin_config["async-threshold"]: - execute_pass(solid_pass) - execute_pass(dependent_pass) - else: - execute_pass(solid_pass) - execute_pass(dependent_pass) - - sender.send_message(f"Operation complete ({len(blocks_to_change)} blocks affected).") + sender.send_message(f"Operation complete ({affected_blocks} blocks affected).") return True sender.send_message(f"Unknown sub-command '{sub_command}'. Use load or list.") - return False \ No newline at end of file + return False diff --git a/src/endstone_worldedit/commands/schem.py b/src/endstone_worldedit/commands/schem.py index c75d83a..824d623 100644 --- a/src/endstone_worldedit/commands/schem.py +++ b/src/endstone_worldedit/commands/schem.py @@ -185,7 +185,7 @@ def handler(plugin, sender, args): # Function to execute a pass def execute_pass(blocks_pass): - if len(blocks_pass) > plugin.plugin_config["async-threshold"]: + if len(blocks_pass) > plugin.plugin_config.get("async-threshold", 5000): plugin.tasks[player_uuid] = {"dimension": dimension, "blocks": blocks_pass} sender.send_message(f"Starting async operation for {len(blocks_pass)} blocks...") else: @@ -193,9 +193,8 @@ def execute_pass(blocks_pass): try: block = dimension.get_block_at(x, y, z) block.set_type(block_type) - # if data_value is not None: - # # block.data = data_value # Endstone API is read-only - # pass + if data_value is not None: + block.set_data(data_value) except RuntimeError as e: plugin.logger.error(f"Skipping block '{block_type}' for player {sender.name}: {e}") sender.send_message(f"§cSkipped block: {block_type} ({e})§r") diff --git a/src/endstone_worldedit/config/configs.py b/src/endstone_worldedit/config/configs.py new file mode 100644 index 0000000..314ae67 --- /dev/null +++ b/src/endstone_worldedit/config/configs.py @@ -0,0 +1,91 @@ +import os +from ruamel.yaml import YAML + +DEFAULT_BLOCK_TRANSLATION_MAP = { + "cobblestone_stairs": "stone_stairs", + "rooted_dirt": "dirt", + "flowering_azalea_leaves": "azalea_leaves_flowered", + "slime_block": "slime", + "sugar_cane": "reeds", + "small_dripleaf": "small_dripleaf_block", + "magma_block": "magma", + "lily_pad": "waterlily", + "dead_bush": "deadbush", + "snow_block": "snow", + "dirt_path": "grass_path", + "jack_o_lantern": "lit_pumpkin", + "melon": "melon_block", + "end_stone_bricks": "end_bricks", + "end_stone_brick_stairs": "end_brick_stairs", + "prismarine_brick_stairs": "prismarine_stairs", + "nether_bricks": "nether_brick", + "bricks": "brick_block", + "red_nether_bricks": "red_nether_brick", + "note_block": "noteblock", + "cobweb": "web", + "nether_quartz_ore": "quartz_ore", + "waxed_copper_block": "copper_block", + "repeater": "unpowered_repeater", + "comparator": "unpowered_comparator", + "powered_rail": "golden_rail", + "beetroots": "beetroot", + "oak_door": "wooden_door", + "oak_trapdoor": "trapdoor", + "oak_fence": "fence", + "oak_fence_gate": "fence_gate", + "oak_button": "wooden_button", + "oak_pressure_plate": "wooden_pressure_plate", + "oak_sign": "standing_sign", + "oak_wall_sign": "wall_sign", + "warped_sign": "standing_sign", + "warped_wall_sign": "wall_sign", + "crimson_sign": "standing_sign", + "crimson_wall_sign": "wall_sign", + "bamboo_sign": "standing_sign", + "bamboo_wall_sign": "wall_sign", + "cherry_sign": "standing_sign", + "cherry_wall_sign": "wall_sign", + "mangrove_sign": "standing_sign", + "mangrove_wall_sign": "wall_sign", + "jungle_sign": "standing_sign", + "jungle_wall_sign": "wall_sign", + "acacia_sign": "standing_sign", + "acacia_wall_sign": "wall_sign", + "dark_oak_sign": "standing_sign", + "dark_oak_wall_sign": "wall_sign", + "birch_sign": "standing_sign", + "birch_wall_sign": "wall_sign", + "spruce_sign": "standing_sign", + "spruce_wall_sign": "wall_sign", + "oak_wall_hanging_sign": "wall_sign", + "terracotta": "hardened_clay", + "light_gray_glazed_terracotta": "light_gray_concrete", + "dark_oak_standing_sign": "standing_sign", + "sign": "standing_sign", +} + +DEFAULT_CONFIG = { + "async-threshold": 5000, + "particle-type": "minecraft:endrod", + "particle-density-step": 5, + "schematic-path": "plugins/WorldEdit/schematics", + "mcstructures-path": "plugins/WorldEdit/mcstructures", + "block_translation_map": DEFAULT_BLOCK_TRANSLATION_MAP +} + +def load_plugin_config(config_path: str = "plugins/WorldEdit/config.yml") -> dict: + yaml = YAML() + yaml.indent(mapping=4, sequence=4, offset=2) + + if not os.path.exists(config_path): + os.makedirs(os.path.dirname(config_path), exist_ok=True) + config = DEFAULT_CONFIG + with open(config_path, 'w', encoding='utf-8') as f: + yaml.dump(config, f) + else: + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.load(f) + if config is None: + config = DEFAULT_CONFIG + + return config diff --git a/src/endstone_worldedit/plugin.py b/src/endstone_worldedit/plugin.py index 06657d5..a3dfe10 100644 --- a/src/endstone_worldedit/plugin.py +++ b/src/endstone_worldedit/plugin.py @@ -7,11 +7,9 @@ ) import time import os -import json from endstone.command import Command, CommandSender, CommandSenderWrapper from .commands import preloaded_commands, preloaded_handlers - class WorldEditPlugin(Plugin): api_version = "0.11" commands = preloaded_commands @@ -38,87 +36,8 @@ def on_load(self): os.makedirs(mcstructures_path) def load_config(self): - config_path = "plugins/WorldEdit/config.json" - default_block_translation_map = { - "cobblestone_stairs": "stone_stairs", - "rooted_dirt": "dirt", - "flowering_azalea_leaves": "azalea_leaves_flowered", - "slime_block": "slime", - "sugar_cane": "reeds", - "small_dripleaf": "small_dripleaf_block", - "magma_block": "magma", - "lily_pad": "waterlily", - "dead_bush": "deadbush", - "snow_block": "snow", - "dirt_path": "grass_path", - "jack_o_lantern": "lit_pumpkin", - "melon": "melon_block", - "end_stone_bricks": "end_bricks", - "end_stone_brick_stairs": "end_brick_stairs", - "prismarine_brick_stairs": "prismarine_stairs", - "nether_bricks": "nether_brick", - "bricks": "brick_block", - "red_nether_bricks": "red_nether_brick", - "note_block": "noteblock", - "cobweb": "web", - "nether_quartz_ore": "quartz_ore", - "waxed_copper_block": "copper_block", - "repeater": "unpowered_repeater", - "comparator": "unpowered_comparator", - "powered_rail": "golden_rail", - "beetroots": "beetroot", - "oak_door": "wooden_door", - "oak_trapdoor": "trapdoor", - "oak_fence": "fence", - "oak_fence_gate": "fence_gate", - "oak_button": "wooden_button", - "oak_pressure_plate": "wooden_pressure_plate", - "oak_sign": "standing_sign", - "oak_wall_sign": "wall_sign", - "warped_sign": "standing_sign", - "warped_wall_sign": "wall_sign", - "crimson_sign": "standing_sign", - "crimson_wall_sign": "wall_sign", - "bamboo_sign": "standing_sign", - "bamboo_wall_sign": "wall_sign", - "cherry_sign": "standing_sign", - "cherry_wall_sign": "wall_sign", - "mangrove_sign": "standing_sign", - "mangrove_wall_sign": "wall_sign", - "jungle_sign": "standing_sign", - "jungle_wall_sign": "wall_sign", - "acacia_sign": "standing_sign", - "acacia_wall_sign": "wall_sign", - "dark_oak_sign": "standing_sign", - "dark_oak_wall_sign": "wall_sign", - "birch_sign": "standing_sign", - "birch_wall_sign": "wall_sign", - "spruce_sign": "standing_sign", - "spruce_wall_sign": "wall_sign", - "oak_wall_hanging_sign": "wall_sign", - "terracotta": "hardened_clay", - "light_gray_glazed_terracotta": "light_gray_concrete", - "dark_oak_standing_sign": "standing_sign", - "sign": "standing_sign", - } - default_config = { - "async-threshold": 5000, - "particle-type": "minecraft:endrod", - "particle-density-step": 5, - "schematic-path": "plugins/WorldEdit/schematics", - "mcstructures-path": "plugins/WorldEdit/mcstructures", - "block_translation_map": default_block_translation_map - } - - if not os.path.exists(config_path): - os.makedirs(os.path.dirname(config_path), exist_ok=True) - self.plugin_config = default_config - with open(config_path, 'w') as f: - json.dump(self.plugin_config, f, indent=4) - else: - with open(config_path, 'r') as f: - self.plugin_config = json.load(f) - + from .config.configs import load_plugin_config + self.plugin_config = load_plugin_config() self.block_translation_map = self.plugin_config.get("block_translation_map", {}) def on_enable(self): From 32cdfb4e01fdc0dea4dedf5d7ccd425d61c06ceb Mon Sep 17 00:00:00 2001 From: MakiTazo Date: Sat, 13 Jun 2026 14:56:23 -0600 Subject: [PATCH 6/6] Updated --- README.md | 46 +++++------ src/endstone_worldedit/commands/mcs.py | 106 +++++++++++++------------ 2 files changed, 77 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 44de239..921c868 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ A powerful and intuitive WorldEdit-like plugin for the Endstone Minecraft server - **Advanced Clipboard**: Copy (`/copy`), cut (`/cut`), and paste (`/paste`) complex structures relative to your position. - **Reliable History**: Easily undo (`/undo`) and redo (`/redo`) your actions to correct mistakes without hassle. - **Procedural Generation**: Create perfect solid (`/sphere`, `/cyl`) and hollow (`/hsphere`, `/hcyl`) shapes like spheres and cylinders. -- **Cross-Edition Schematics**: Save and load structures to and from `.schem` files. The plugin is designed to handle compatibility between Java Edition and Bedrock Edition formats. -- **Flexible Configuration**: Customize plugin behavior and, most importantly, add custom block name translations via `config.json` to resolve schematic compatibility issues on the fly. +- **Cross-Edition Schematics & Structures**: Save and load structures to and from `.schem` and `.mcstructure` files. +- **Flexible Configuration**: Customize plugin behavior and add custom block name translations via `config.yml` to resolve compatibility issues. - **Granular Permissions**: Fine-grained permission nodes for every command (e.g., `worldedit.command.set`) for precise server management. ## Installation @@ -34,30 +34,28 @@ A powerful and intuitive WorldEdit-like plugin for the Endstone Minecraft server ## Configuration -Upon first launch, the plugin will create a `config.json` file in `plugins/WorldEdit/`. This file allows you to customize the plugin's behavior. - -**Note:** When updating the plugin to a new version, it is recommended to delete your existing `config.json` file. This allows the plugin to generate a new one with the latest default settings and translation rules. - -```json -{ - "async-threshold": 5000, - "particle-type": "minecraft:endrod", - "particle-density-step": 5, - "schematic-path": "plugins/WorldEdit/schematics", - "block_translation_map": { - "cobblestone_stairs": "stone_stairs", - "rooted_dirt": "dirt", - "sugar_cane": "reeds", - "slime_block": "slime", - "oak_sign": "standing_sign", - "oak_wall_sign": "wall_sign" - } -} +Upon first launch, the plugin will create a `config.yml` file in `plugins/WorldEdit/`. This file allows you to customize the plugin's behavior. + +**Note:** When updating the plugin to a new version, it is recommended to delete your existing `config.yml` file to let the plugin generate a new one with the latest default settings. + +```yaml +async-threshold: 5000 +particle-type: minecraft:endrod +particle-density-step: 5 +schematic-path: plugins/WorldEdit/schematics +mcstructures-path: plugins/WorldEdit/mcstructures +block_translation_map: + cobblestone_stairs: stone_stairs + rooted_dirt: dirt + sugar_cane: reeds + slime_block: slime + oak_sign: standing_sign + oak_wall_sign: wall_sign ``` - **`async-threshold`**: The number of blocks at which an operation will be processed in smaller chunks to prevent server lag. - **`particle-type`**: The particle used to visualize the selection box. -- **`block_translation_map`**: A powerful feature for resolving compatibility issues when loading Java Edition schematics. If a schematic fails to load because a block name isn't found (e.g., Java's `minecraft:slime_block`), you can add an entry here to translate it to the correct Bedrock name (e.g., `"slime_block": "slime"`). You can add, remove, or modify these rules at any time to handle new or custom block types. +- **`block_translation_map`**: A powerful feature for resolving compatibility issues when loading Java Edition schematics. If a schematic fails to load because a block name isn't found, add an entry to translate it to the correct Bedrock name. ## Usage Guide @@ -98,11 +96,13 @@ Create perfect geometric shapes. - **/cyl `` `` `[height]`**: Creates a solid cylinder. - **/hcyl `` `` `[height]`**: Creates a hollow cylinder. -### 6. Schematics +### 6. Schematics & Structures Save and load your creations. - **/schem save ``**: Saves the selection to `.schem`. - **/schem load ``**: Loads a schematic file at your location. - **/schem list**: Lists all available schematics. +- **/mcs load ``**: Loads a Bedrock `.mcstructure` file at your location. +- **/mcs list**: Lists all available mcstructures. ## Contributing diff --git a/src/endstone_worldedit/commands/mcs.py b/src/endstone_worldedit/commands/mcs.py index b140522..df274c9 100644 --- a/src/endstone_worldedit/commands/mcs.py +++ b/src/endstone_worldedit/commands/mcs.py @@ -23,30 +23,6 @@ "minecraft:flowing_lava", } - -def load_mcstructure(file_path): - nbt_file = nbtlib.load(file_path, byteorder='little') - if "" in nbt_file.keys(): - nbt_file = nbt_file[""] - blocks_raw = list(map(int, nbt_file["structure"]["block_indices"][0])) - size = list(map(int, nbt_file["size"])) - palette = nbt_file["structure"]["palette"]["default"]["block_palette"] - return blocks_raw, size, palette - - -def get_block(blocks_raw, size, palette, x, y, z): - width, height, length = size - if x < 0 or y < 0 or z < 0 or x >= width or y >= height or z >= length: - return None - idx = (x * height + y) * length + z - if idx < 0 or idx >= len(blocks_raw): - return None - palette_index = blocks_raw[idx] - if palette_index < 0 or palette_index >= len(palette): - return None - return palette[palette_index] - - command = { "mcs": { "description": "Loads a .mcstructure file.", @@ -58,7 +34,6 @@ def get_block(blocks_raw, size, palette, x, y, z): } } - @command_executor("mcs") def handler(plugin, sender, args): if len(args) < 1: @@ -94,7 +69,12 @@ def handler(plugin, sender, args): return False try: - blocks_raw, size, palette = load_mcstructure(file_path) + nbt_file = nbtlib.load(file_path, byteorder='little') + if "" in nbt_file.keys(): + nbt_file = nbt_file[""] + blocks_raw = list(map(int, nbt_file["structure"]["block_indices"][0])) + size = list(map(int, nbt_file["size"])) + palette = nbt_file["structure"]["palette"]["default"]["block_palette"] except Exception as e: sender.send_message(f"Error reading structure file: {e}") return False @@ -104,15 +84,21 @@ def handler(plugin, sender, args): dimension = sender.dimension player_location = sender.location sender.send_message(f"Loading structure ({width}x{height}x{length})...") - blocks_to_change = [] for y in range(height): for z in range(length): for x in range(width): - block_info = get_block(blocks_raw, size, palette, x, y, z) - if block_info is None: + idx = (x * height + y) * length + z + if idx < 0 or idx >= len(blocks_raw): continue + palette_index = blocks_raw[idx] + if palette_index < 0 or palette_index >= len(palette): + continue + block_info = palette[palette_index] + block_name = str(block_info['name']) + if block_name in ("minecraft:air", "minecraft:flowing_water", "minecraft:flowing_lava"): + continue if block_name in BLOCKED_BLOCKS: continue target_x = int(player_location.x) + x @@ -135,40 +121,56 @@ def handler(plugin, sender, args): sender.send_message("Structure is empty or only contains air.") return True - affected_blocks = len(blocks_to_change) - sender.send_message(f"Placing {affected_blocks} blocks...") - - # Store undo history first - undo_entry = [] - plugin.redo_history[player_uuid] = [] - + sender.send_message(f"Placing {len(blocks_to_change)} blocks...") + + dependent_blocks = [ + "flower", "sapling", "mushroom", "torch", "rail", "redstone_wire", "repeater", "comparator", + "sign", "door", "lever", "button", "pressure_plate", "tripwire_hook", "tripwire", "banner" + ] + solid_pass = [b for b in blocks_to_change if not any(d in b[3] for d in dependent_blocks)] + dependent_pass = [b for b in blocks_to_change if any(d in b[3] for d in dependent_blocks)] + + full_undo_entry = [] for x, y, z, _, _ in blocks_to_change: try: - block = dimension.get_block_at(x, y, z) - undo_entry.append((x, y, z, block.type, block.data)) + old = dimension.get_block_at(x, y, z) + full_undo_entry.append((x, y, z, str(old.type), old.data)) except RuntimeError: continue - + if player_uuid not in plugin.undo_history: plugin.undo_history[player_uuid] = [] - plugin.undo_history[player_uuid].append(undo_entry) + plugin.undo_history[player_uuid].append(full_undo_entry) + plugin.redo_history[player_uuid] = [] - # Execute asynchronously if the task is large - if affected_blocks > plugin.plugin_config["async-threshold"]: - plugin.tasks[player_uuid] = {"dimension": dimension, "blocks": blocks_to_change} - sender.send_message(f"Starting async operation for {affected_blocks} blocks...") - else: - for x, y, z, block_type, states in blocks_to_change: + total_blocks = len(blocks_to_change) + if total_blocks > plugin.plugin_config.get("async-threshold", 5000): + ordered_blocks = solid_pass + dependent_pass + async_blocks = [] + for x, y, z, block_type, states in ordered_blocks: try: - block = dimension.get_block_at(x, y, z) bd = plugin.server.create_block_data(block_type, block_states=states) - block.set_type(block_type) - block.set_data(bd) + async_blocks.append((x, y, z, block_type, bd)) except Exception as e: - plugin.logger.error(f"Failed block {block_type}: {e}") - continue + plugin.logger.error(f"Failed to create block data for {block_type}: {e}") + + plugin.tasks[player_uuid] = {"dimension": dimension, "blocks": async_blocks} + sender.send_message(f"Starting async operation for {total_blocks} blocks...") + else: + def execute_pass(blocks_pass): + for x, y, z, block_type, states in blocks_pass: + try: + block = dimension.get_block_at(x, y, z) + bd = plugin.server.create_block_data(block_type, block_states=states) + block.set_type(block_type) + block.set_data(bd) + except Exception as e: + plugin.logger.error(f"Failed block {block_type}: {e}") + continue + execute_pass(solid_pass) + execute_pass(dependent_pass) - sender.send_message(f"Operation complete ({affected_blocks} blocks affected).") + sender.send_message(f"Operation complete ({len(blocks_to_change)} blocks affected).") return True sender.send_message(f"Unknown sub-command '{sub_command}'. Use load or list.")