From 856d8cf4244cfc2048064c927ce72bdd2878f89a Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Sat, 28 Feb 2026 17:49:09 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(command):=20=E6=B7=BB=E5=8A=A0box3expo?= =?UTF-8?q?rt=E5=91=BD=E4=BB=A4=E6=94=AF=E6=8C=81=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了完整的box3export命令系统,支持多种方式导出区域: - 使用标记方块自动识别导出范围 - 通过pos1/pos2手动设置导出位置 - 支持直接坐标输入或玩家当前位置 - 提供选择清除和状态管理功能 同时更新了BlockIdResolver工具类以支持方块ID双向映射, 并添加了中英文语言文件中的相关提示信息。 --- .../command/Box3ExportSelectionStore.java | 43 +++ .../java/com/box3lab/command/ModCommands.java | 255 ++++++++++++++++++ .../com/box3lab/register/VoxelExport.java | 148 ++++++++++ .../com/box3lab/util/BlockIdResolver.java | 33 +++ .../resources/assets/box3/lang/en_us.json | 8 + .../resources/assets/box3/lang/zh_cn.json | 8 + 6 files changed, 495 insertions(+) create mode 100644 Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java create mode 100644 Fabric-1.21.11/src/main/java/com/box3lab/register/VoxelExport.java diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java b/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java new file mode 100644 index 0000000..a7d5b70 --- /dev/null +++ b/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java @@ -0,0 +1,43 @@ +package com.box3lab.command; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import net.minecraft.core.BlockPos; + +final class Box3ExportSelectionStore { + + private static final Map SELECTIONS = new ConcurrentHashMap<>(); + + private Box3ExportSelectionStore() { + } + + static Selection get(UUID playerId) { + return SELECTIONS.get(playerId); + } + + static Selection setPos1(UUID playerId, BlockPos pos) { + Selection current = SELECTIONS.get(playerId); + Selection updated = new Selection(pos.immutable(), current != null ? current.pos2() : null); + SELECTIONS.put(playerId, updated); + return updated; + } + + static Selection setPos2(UUID playerId, BlockPos pos) { + Selection current = SELECTIONS.get(playerId); + Selection updated = new Selection(current != null ? current.pos1() : null, pos.immutable()); + SELECTIONS.put(playerId, updated); + return updated; + } + + static void clear(UUID playerId) { + SELECTIONS.remove(playerId); + } + + record Selection(BlockPos pos1, BlockPos pos2) { + boolean complete() { + return pos1 != null && pos2 != null; + } + } +} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java b/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java index bb43fb7..bc92a3a 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java @@ -1,9 +1,11 @@ package com.box3lab.command; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import com.box3lab.block.BarrierVoxelBlock; +import com.box3lab.register.VoxelExport; import com.box3lab.register.VoxelImport; import com.box3lab.util.Box3ImportFiles; import com.mojang.brigadier.arguments.BoolArgumentType; @@ -16,14 +18,20 @@ import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Block; public final class ModCommands { private ModCommands() { } + private static final String DEFAULT_EXPORT_MARKER_BLOCK = "minecraft:redstone_block"; + private static final int MARKER_SCAN_RADIUS = 64; + private static final SuggestionProvider BOX3_FILE_SUGGESTIONS = (context, builder) -> { try { List files = Box3ImportFiles.listJsonFiles(); @@ -125,6 +133,96 @@ public static void register() { .then(literal("toggle") .executes(context -> toggleBarrierVisible( context.getSource()))))); + + dispatcher.register( + literal("box3export") + .then(literal("marker") + .then(argument("fileName", StringArgumentType.word()) + .executes(context -> executeBox3ExportByMarkers( + context.getSource(), + StringArgumentType + .getString( + context, + "fileName"), + DEFAULT_EXPORT_MARKER_BLOCK)) + .then(argument("markerBlock", + StringArgumentType.word()) + .executes(context -> executeBox3ExportByMarkers( + context.getSource(), + StringArgumentType + .getString( + context, + "fileName"), + StringArgumentType + .getString( + context, + "markerBlock")))))) + .then(literal("pos1") + .executes(context -> setExportPosFromPlayer( + context.getSource(), + true)) + .then(argument("x", IntegerArgumentType.integer()) + .then(argument("y", + IntegerArgumentType.integer()) + .then(argument("z", + IntegerArgumentType.integer()) + .executes(context -> setExportPos( + context.getSource(), + true, + IntegerArgumentType.getInteger( + context, + "x"), + IntegerArgumentType.getInteger( + context, + "y"), + IntegerArgumentType.getInteger( + context, + "z"))))))) + .then(literal("pos2") + .executes(context -> setExportPosFromPlayer( + context.getSource(), + false)) + .then(argument("x", IntegerArgumentType.integer()) + .then(argument("y", + IntegerArgumentType.integer()) + .then(argument("z", + IntegerArgumentType.integer()) + .executes(context -> setExportPos( + context.getSource(), + false, + IntegerArgumentType.getInteger( + context, + "x"), + IntegerArgumentType.getInteger( + context, + "y"), + IntegerArgumentType.getInteger( + context, + "z"))))))) + .then(literal("clear") + .executes(context -> clearExportSelection( + context.getSource()))) + .then(argument("fileName", StringArgumentType.word()) + .executes(context -> executeBox3ExportBySelection( + context.getSource(), + StringArgumentType.getString( + context, + "fileName"))) + .then(argument("x1", IntegerArgumentType.integer()) + .then(argument("y1", IntegerArgumentType.integer()) + .then(argument("z1", IntegerArgumentType.integer()) + .then(argument("x2", IntegerArgumentType.integer()) + .then(argument("y2", IntegerArgumentType.integer()) + .then(argument("z2", IntegerArgumentType.integer()) + .executes(context -> executeBox3Export( + context.getSource(), + StringArgumentType.getString(context, "fileName"), + IntegerArgumentType.getInteger(context, "x1"), + IntegerArgumentType.getInteger(context, "y1"), + IntegerArgumentType.getInteger(context, "z1"), + IntegerArgumentType.getInteger(context, "x2"), + IntegerArgumentType.getInteger(context, "y2"), + IntegerArgumentType.getInteger(context, "z2"))))))))))); }); } @@ -221,4 +319,161 @@ private static int toggleBarrierVisible(CommandSourceStack source) { false); return 1; } + + private static int executeBox3Export(CommandSourceStack source, String fileName, + int x1, int y1, int z1, int x2, int y2, int z2) { + ServerLevel level = source.getLevel(); + BlockPos from = new BlockPos(x1, y1, z1); + BlockPos to = new BlockPos(x2, y2, z2); + + try { + VoxelExport.ExportResult result = VoxelExport.exportRegion(level, from, to, fileName); + source.sendSuccess( + () -> Component.translatable( + "command.box3.box3export.success", + result.output().toString(), + result.scannedBlocks(), + result.exportedBlocks()), + false); + } catch (Exception e) { + source.sendFailure( + Component.translatable("command.box3.box3export.failure", e.getMessage())); + } + return 1; + } + + private static int executeBox3ExportBySelection(CommandSourceStack source, String fileName) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + + var selection = Box3ExportSelectionStore.get(player.getUUID()); + if (selection == null || !selection.complete()) { + source.sendFailure(Component.translatable("command.box3.box3export.selection_incomplete")); + return 0; + } + + return executeBox3Export( + source, + fileName, + selection.pos1().getX(), + selection.pos1().getY(), + selection.pos1().getZ(), + selection.pos2().getX(), + selection.pos2().getY(), + selection.pos2().getZ()); + } + + private static int setExportPosFromPlayer(CommandSourceStack source, boolean firstPos) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + BlockPos pos = player.blockPosition(); + return setExportPos(source, firstPos, pos.getX(), pos.getY(), pos.getZ()); + } + + private static int setExportPos(CommandSourceStack source, boolean firstPos, int x, int y, int z) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + + BlockPos pos = new BlockPos(x, y, z); + if (firstPos) { + Box3ExportSelectionStore.setPos1(player.getUUID(), pos); + source.sendSuccess( + () -> Component.translatable("command.box3.box3export.pos_set", "pos1", x, y, z), + false); + } else { + Box3ExportSelectionStore.setPos2(player.getUUID(), pos); + source.sendSuccess( + () -> Component.translatable("command.box3.box3export.pos_set", "pos2", x, y, z), + false); + } + return 1; + } + + private static int clearExportSelection(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + + Box3ExportSelectionStore.clear(player.getUUID()); + source.sendSuccess(() -> Component.translatable("command.box3.box3export.selection_cleared"), false); + return 1; + } + + private static int executeBox3ExportByMarkers(CommandSourceStack source, String fileName, String markerBlockId) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + + Block markerBlock = resolveMarkerBlock(markerBlockId); + if (markerBlock == null) { + source.sendFailure(Component.translatable("command.box3.box3export.marker_invalid", markerBlockId)); + return 0; + } + + List positions = findMarkerPositions(source.getLevel(), player.blockPosition(), markerBlock, + MARKER_SCAN_RADIUS, 3); + if (positions.size() != 2) { + source.sendFailure(Component.translatable( + "command.box3.box3export.marker_count_invalid", + MARKER_SCAN_RADIUS, + positions.size(), + BuiltInRegistries.BLOCK.getKey(markerBlock).toString())); + return 0; + } + + BlockPos p1 = positions.get(0); + BlockPos p2 = positions.get(1); + return executeBox3Export(source, fileName, p1.getX(), p1.getY(), p1.getZ(), p2.getX(), p2.getY(), p2.getZ()); + } + + private static Block resolveMarkerBlock(String blockId) { + Identifier id = Identifier.tryParse(blockId); + if (id == null) { + return null; + } + if (!BuiltInRegistries.BLOCK.containsKey(id)) { + return null; + } + return BuiltInRegistries.BLOCK.get(id).map(holder -> holder.value()).orElse(null); + } + + private static List findMarkerPositions(ServerLevel level, BlockPos center, Block markerBlock, int radius, + int maxResults) { + List positions = new ArrayList<>(); + int minX = center.getX() - radius; + int maxX = center.getX() + radius; + int minY = center.getY() - radius; + int maxY = center.getY() + radius; + int minZ = center.getZ() - radius; + int maxZ = center.getZ() + radius; + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + + for (int y = minY; y <= maxY; y++) { + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + cursor.set(x, y, z); + if (level.getBlockState(cursor).getBlock() == markerBlock) { + positions.add(cursor.immutable()); + if (positions.size() >= maxResults) { + return positions; + } + } + } + } + } + return positions; + } } diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/register/VoxelExport.java b/Fabric-1.21.11/src/main/java/com/box3lab/register/VoxelExport.java new file mode 100644 index 0000000..6873594 --- /dev/null +++ b/Fabric-1.21.11/src/main/java/com/box3lab/register/VoxelExport.java @@ -0,0 +1,148 @@ +package com.box3lab.register; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import com.box3lab.util.BlockIdResolver; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +public final class VoxelExport { + + private VoxelExport() { + } + + public static ExportResult exportRegion(ServerLevel level, BlockPos from, BlockPos to, String fileName) throws IOException { + BlockPos min = new BlockPos( + Math.min(from.getX(), to.getX()), + Math.min(from.getY(), to.getY()), + Math.min(from.getZ(), to.getZ())); + BlockPos max = new BlockPos( + Math.max(from.getX(), to.getX()), + Math.max(from.getY(), to.getY()), + Math.max(from.getZ(), to.getZ())); + + int sizeX = max.getX() - min.getX() + 1; + int sizeY = max.getY() - min.getY() + 1; + int sizeZ = max.getZ() - min.getZ() + 1; + + List indices = new ArrayList<>(); + List data = new ArrayList<>(); + List rot = new ArrayList<>(); + + for (int z = 0; z < sizeZ; z++) { + for (int y = 0; y < sizeY; y++) { + for (int x = 0; x < sizeX; x++) { + BlockPos pos = min.offset(x, y, z); + BlockState state = level.getBlockState(pos); + int id = BlockIdResolver.getIdByBlock(state.getBlock()); + if (id == 0) { + continue; + } + + int idx = x + y * sizeX + z * sizeX * sizeY; + indices.add(idx); + data.add(id); + rot.add(toRotationIndex(state)); + } + } + } + + JsonObject root = new JsonObject(); + root.add("shape", intArray(sizeX, sizeY, sizeZ)); + root.add("dir", intArray(1, 1, 1)); + root.add("indices", toJsonArray(indices)); + root.add("data", toJsonArray(data)); + root.add("rot", toJsonArray(rot)); + + Path output = resolveOutput(fileName); + Files.createDirectories(output.getParent()); + writeGzipJson(output, root.toString()); + + return new ExportResult(output, sizeX * sizeY * sizeZ, indices.size()); + } + + private static int toRotationIndex(BlockState state) { + Direction dir = null; + if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) { + dir = state.getValue(BlockStateProperties.HORIZONTAL_FACING); + } else if (state.hasProperty(BlockStateProperties.FACING)) { + Direction facing = state.getValue(BlockStateProperties.FACING); + if (facing.getAxis().isHorizontal()) { + dir = facing; + } + } + + if (dir == null) { + return 0; + } + + Rotation rotation = switch (dir) { + case EAST -> Rotation.CLOCKWISE_90; + case SOUTH -> Rotation.CLOCKWISE_180; + case WEST -> Rotation.COUNTERCLOCKWISE_90; + default -> Rotation.NONE; + }; + + return switch (rotation) { + case CLOCKWISE_90 -> 1; + case CLOCKWISE_180 -> 2; + case COUNTERCLOCKWISE_90 -> 3; + default -> 0; + }; + } + + private static JsonArray intArray(int a, int b, int c) { + JsonArray array = new JsonArray(); + array.add(a); + array.add(b); + array.add(c); + return array; + } + + private static JsonArray toJsonArray(List values) { + JsonArray array = new JsonArray(); + for (Integer value : values) { + array.add(value); + } + return array; + } + + private static Path resolveOutput(String fileName) { + String cleaned = (fileName == null || fileName.isBlank()) ? "export" : fileName.trim(); + if (!cleaned.endsWith(".gz")) { + cleaned = cleaned + ".gz"; + } + return FabricLoader.getInstance() + .getConfigDir() + .resolve(MOD_ID) + .resolve(cleaned); + } + + private static void writeGzipJson(Path outputPath, String json) throws IOException { + try (OutputStream fos = Files.newOutputStream(outputPath); + GZIPOutputStream gos = new GZIPOutputStream(fos)) { + gos.write(json.getBytes(StandardCharsets.UTF_8)); + gos.finish(); + } + } + + public record ExportResult(Path output, int scannedBlocks, int exportedBlocks) { + } +} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/util/BlockIdResolver.java b/Fabric-1.21.11/src/main/java/com/box3lab/util/BlockIdResolver.java index 173c015..2b4b649 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/util/BlockIdResolver.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/util/BlockIdResolver.java @@ -3,12 +3,15 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import com.box3lab.register.ModBlocks; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -16,6 +19,7 @@ public final class BlockIdResolver { private static JsonObject blockIdMapping = null; + private static Map blockToIdMapping = null; private BlockIdResolver() { } @@ -41,6 +45,20 @@ private static void loadBlockIdMapping() { } } + private static void loadReverseMapping() { + loadBlockIdMapping(); + if (blockToIdMapping != null) { + return; + } + + Map reverse = new HashMap<>(); + for (var entry : blockIdMapping.entrySet()) { + String registryKey = entry.getValue().getAsString().toLowerCase(Locale.ROOT); + reverse.putIfAbsent(registryKey, Integer.parseInt(entry.getKey())); + } + blockToIdMapping = reverse; + } + public static Block getBlockById(int id) { return getBlockById(id, false); } @@ -99,4 +117,19 @@ public static boolean isBarrierId(int id) { String registryKey = blockIdMapping.get(idStr).getAsString(); return "barrier".equalsIgnoreCase(registryKey); } + + public static int getIdByBlock(Block block) { + loadReverseMapping(); + String key = BuiltInRegistries.BLOCK.getKey(block).getPath().toLowerCase(Locale.ROOT); + Integer id = blockToIdMapping.get(key); + if (id != null) { + return id; + } + + if (block == Blocks.WATER) { + return 364; + } + + return 0; + } } diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json index 00c4e98..d91c4a0 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json @@ -411,6 +411,14 @@ "command.box3.box3barrier.status": "Barrier visible: %s", "command.box3.box3barrier.set": "Barrier visibility set to: %s", "command.box3.box3barrier.toggled": "Barrier visibility toggled to: %s (re-enter the world to fully apply)", + "command.box3.box3export.success": "Exported successfully: %s (scanned %s blocks, exported %s)", + "command.box3.box3export.failure": "Export failed: %s", + "command.box3.box3export.player_only": "This command can only be used by a player.", + "command.box3.box3export.selection_incomplete": "Selection is incomplete. Set both pos1 and pos2 first.", + "command.box3.box3export.selection_cleared": "Export selection cleared.", + "command.box3.box3export.pos_set": "Set %s to (%s, %s, %s).", + "command.box3.box3export.marker_invalid": "Invalid marker block id: %s", + "command.box3.box3export.marker_count_invalid": "Within radius %s, found %s marker blocks (%s), but exactly 2 are required.", "message.box3.model.config.mode": "Model config mode: %s (Stick: +, Blaze Rod: -, Paper: copy, Book: paste)", "message.box3.model.config.status": "mode=%s scale=%s offset=(%s, %s, %s) rot=%s", "message.box3.model.config.mode.scale": "Scale", diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json index 82b8d1e..63fe7a4 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json @@ -399,6 +399,14 @@ "command.box3.box3barrier.status": "屏障可见状态:%s", "command.box3.box3barrier.set": "屏障可见状态已设置为:%s", "command.box3.box3barrier.toggled": "屏障可见状态已切换为:%s(重新进入世界以完全生效)", + "command.box3.box3export.success": "导出成功:%s(扫描 %s 方块,导出 %s 个)", + "command.box3.box3export.failure": "导出失败:%s", + "command.box3.box3export.player_only": "该命令只能由玩家执行。", + "command.box3.box3export.selection_incomplete": "选区不完整,请先设置 pos1 和 pos2。", + "command.box3.box3export.selection_cleared": "已清空导出选区。", + "command.box3.box3export.pos_set": "已设置 %s:(%s, %s, %s)", + "command.box3.box3export.marker_invalid": "无效的标记方块ID:%s", + "command.box3.box3export.marker_count_invalid": "在半径 %s 内找到 %s 个标记方块(%s),需要刚好 2 个。", "message.box3.model.config.mode": "模型配置模式:%s(木棍:增加,烈焰棒:减少,纸:复制,书:粘贴)", "message.box3.model.config.status": "模式=%s 缩放=%s 偏移=(%s, %s, %s) 旋转=%s", "message.box3.model.config.mode.scale": "缩放", From 8a7cf450b6832e8a8eebddd11edd517d628325fd Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Sat, 28 Feb 2026 18:11:48 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(command):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E9=80=89=E6=8B=A9=E4=BD=8D=E7=BD=AE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=A0=87=E8=AE=B0=E6=89=AB?= =?UTF-8?q?=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了 Box3ExportSelectionStore 类及相关的手动设置 pos1/pos2 功能 - 将 /box3export 命令重构为仅支持标记块模式,移除了 pos1/pos2/clear 子命令 - 新增显示用法提示的功能,当执行 /box3export 无参数时显示使用说明 - 增加最大标记扫描半径至 1024,新增 Y 轴容差 512,优化扫描算法 - 改进标记块查找逻辑,改为从中心向外逐层扫描以提高性能 - 更新中英文语言文件中的命令使用说明 --- .../command/Box3ExportSelectionStore.java | 43 --- .../java/com/box3lab/command/ModCommands.java | 260 ++++++------------ .../resources/assets/box3/lang/en_us.json | 3 +- .../resources/assets/box3/lang/zh_cn.json | 3 +- 4 files changed, 88 insertions(+), 221 deletions(-) delete mode 100644 Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java b/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java deleted file mode 100644 index a7d5b70..0000000 --- a/Fabric-1.21.11/src/main/java/com/box3lab/command/Box3ExportSelectionStore.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.box3lab.command; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import net.minecraft.core.BlockPos; - -final class Box3ExportSelectionStore { - - private static final Map SELECTIONS = new ConcurrentHashMap<>(); - - private Box3ExportSelectionStore() { - } - - static Selection get(UUID playerId) { - return SELECTIONS.get(playerId); - } - - static Selection setPos1(UUID playerId, BlockPos pos) { - Selection current = SELECTIONS.get(playerId); - Selection updated = new Selection(pos.immutable(), current != null ? current.pos2() : null); - SELECTIONS.put(playerId, updated); - return updated; - } - - static Selection setPos2(UUID playerId, BlockPos pos) { - Selection current = SELECTIONS.get(playerId); - Selection updated = new Selection(current != null ? current.pos1() : null, pos.immutable()); - SELECTIONS.put(playerId, updated); - return updated; - } - - static void clear(UUID playerId) { - SELECTIONS.remove(playerId); - } - - record Selection(BlockPos pos1, BlockPos pos2) { - boolean complete() { - return pos1 != null && pos2 != null; - } - } -} diff --git a/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java b/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java index bc92a3a..0fa5c70 100644 --- a/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java +++ b/Fabric-1.21.11/src/main/java/com/box3lab/command/ModCommands.java @@ -30,7 +30,8 @@ private ModCommands() { } private static final String DEFAULT_EXPORT_MARKER_BLOCK = "minecraft:redstone_block"; - private static final int MARKER_SCAN_RADIUS = 64; + private static final int MAX_MARKER_SCAN_RADIUS = 1024; + private static final int MARKER_Y_TOLERANCE = 512; private static final SuggestionProvider BOX3_FILE_SUGGESTIONS = (context, builder) -> { try { @@ -136,93 +137,13 @@ public static void register() { dispatcher.register( literal("box3export") - .then(literal("marker") - .then(argument("fileName", StringArgumentType.word()) - .executes(context -> executeBox3ExportByMarkers( - context.getSource(), - StringArgumentType - .getString( - context, - "fileName"), - DEFAULT_EXPORT_MARKER_BLOCK)) - .then(argument("markerBlock", - StringArgumentType.word()) - .executes(context -> executeBox3ExportByMarkers( - context.getSource(), - StringArgumentType - .getString( - context, - "fileName"), - StringArgumentType - .getString( - context, - "markerBlock")))))) - .then(literal("pos1") - .executes(context -> setExportPosFromPlayer( - context.getSource(), - true)) - .then(argument("x", IntegerArgumentType.integer()) - .then(argument("y", - IntegerArgumentType.integer()) - .then(argument("z", - IntegerArgumentType.integer()) - .executes(context -> setExportPos( - context.getSource(), - true, - IntegerArgumentType.getInteger( - context, - "x"), - IntegerArgumentType.getInteger( - context, - "y"), - IntegerArgumentType.getInteger( - context, - "z"))))))) - .then(literal("pos2") - .executes(context -> setExportPosFromPlayer( - context.getSource(), - false)) - .then(argument("x", IntegerArgumentType.integer()) - .then(argument("y", - IntegerArgumentType.integer()) - .then(argument("z", - IntegerArgumentType.integer()) - .executes(context -> setExportPos( - context.getSource(), - false, - IntegerArgumentType.getInteger( - context, - "x"), - IntegerArgumentType.getInteger( - context, - "y"), - IntegerArgumentType.getInteger( - context, - "z"))))))) - .then(literal("clear") - .executes(context -> clearExportSelection( - context.getSource()))) + .executes(context -> showBox3ExportUsage(context.getSource())) .then(argument("fileName", StringArgumentType.word()) - .executes(context -> executeBox3ExportBySelection( + .executes(context -> executeBox3ExportByMarkers( context.getSource(), StringArgumentType.getString( context, - "fileName"))) - .then(argument("x1", IntegerArgumentType.integer()) - .then(argument("y1", IntegerArgumentType.integer()) - .then(argument("z1", IntegerArgumentType.integer()) - .then(argument("x2", IntegerArgumentType.integer()) - .then(argument("y2", IntegerArgumentType.integer()) - .then(argument("z2", IntegerArgumentType.integer()) - .executes(context -> executeBox3Export( - context.getSource(), - StringArgumentType.getString(context, "fileName"), - IntegerArgumentType.getInteger(context, "x1"), - IntegerArgumentType.getInteger(context, "y1"), - IntegerArgumentType.getInteger(context, "z1"), - IntegerArgumentType.getInteger(context, "x2"), - IntegerArgumentType.getInteger(context, "y2"), - IntegerArgumentType.getInteger(context, "z2"))))))))))); + "fileName"))))); }); } @@ -256,6 +177,11 @@ private static int listBox3ImportFiles(CommandSourceStack source) { return 1; } + private static int showBox3ExportUsage(CommandSourceStack source) { + source.sendFailure(Component.translatable("command.box3.box3export.usage")); + return 0; + } + private static String resolveMapName(String fileName) { if (fileName != null && fileName.startsWith("Box3-")) { String suffix = fileName.substring("Box3-".length()); @@ -342,93 +268,26 @@ private static int executeBox3Export(CommandSourceStack source, String fileName, return 1; } - private static int executeBox3ExportBySelection(CommandSourceStack source, String fileName) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - source.sendFailure(Component.translatable("command.box3.box3export.player_only")); - return 0; - } - - var selection = Box3ExportSelectionStore.get(player.getUUID()); - if (selection == null || !selection.complete()) { - source.sendFailure(Component.translatable("command.box3.box3export.selection_incomplete")); - return 0; - } - - return executeBox3Export( - source, - fileName, - selection.pos1().getX(), - selection.pos1().getY(), - selection.pos1().getZ(), - selection.pos2().getX(), - selection.pos2().getY(), - selection.pos2().getZ()); - } - - private static int setExportPosFromPlayer(CommandSourceStack source, boolean firstPos) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - source.sendFailure(Component.translatable("command.box3.box3export.player_only")); - return 0; - } - BlockPos pos = player.blockPosition(); - return setExportPos(source, firstPos, pos.getX(), pos.getY(), pos.getZ()); - } - - private static int setExportPos(CommandSourceStack source, boolean firstPos, int x, int y, int z) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - source.sendFailure(Component.translatable("command.box3.box3export.player_only")); - return 0; - } - - BlockPos pos = new BlockPos(x, y, z); - if (firstPos) { - Box3ExportSelectionStore.setPos1(player.getUUID(), pos); - source.sendSuccess( - () -> Component.translatable("command.box3.box3export.pos_set", "pos1", x, y, z), - false); - } else { - Box3ExportSelectionStore.setPos2(player.getUUID(), pos); - source.sendSuccess( - () -> Component.translatable("command.box3.box3export.pos_set", "pos2", x, y, z), - false); - } - return 1; - } - - private static int clearExportSelection(CommandSourceStack source) { - ServerPlayer player = source.getPlayer(); - if (player == null) { - source.sendFailure(Component.translatable("command.box3.box3export.player_only")); - return 0; - } - - Box3ExportSelectionStore.clear(player.getUUID()); - source.sendSuccess(() -> Component.translatable("command.box3.box3export.selection_cleared"), false); - return 1; - } - - private static int executeBox3ExportByMarkers(CommandSourceStack source, String fileName, String markerBlockId) { + private static int executeBox3ExportByMarkers(CommandSourceStack source, String fileName) { ServerPlayer player = source.getPlayer(); if (player == null) { source.sendFailure(Component.translatable("command.box3.box3export.player_only")); return 0; } - Block markerBlock = resolveMarkerBlock(markerBlockId); + Block markerBlock = resolveMarkerBlock(DEFAULT_EXPORT_MARKER_BLOCK); if (markerBlock == null) { - source.sendFailure(Component.translatable("command.box3.box3export.marker_invalid", markerBlockId)); + source.sendFailure(Component.translatable("command.box3.box3export.marker_invalid", + DEFAULT_EXPORT_MARKER_BLOCK)); return 0; } List positions = findMarkerPositions(source.getLevel(), player.blockPosition(), markerBlock, - MARKER_SCAN_RADIUS, 3); - if (positions.size() != 2) { + MAX_MARKER_SCAN_RADIUS, MARKER_Y_TOLERANCE, 2); + if (positions.size() < 2) { source.sendFailure(Component.translatable( "command.box3.box3export.marker_count_invalid", - MARKER_SCAN_RADIUS, + MAX_MARKER_SCAN_RADIUS, positions.size(), BuiltInRegistries.BLOCK.getKey(markerBlock).toString())); return 0; @@ -436,7 +295,8 @@ private static int executeBox3ExportByMarkers(CommandSourceStack source, String BlockPos p1 = positions.get(0); BlockPos p2 = positions.get(1); - return executeBox3Export(source, fileName, p1.getX(), p1.getY(), p1.getZ(), p2.getX(), p2.getY(), p2.getZ()); + return executeBox3Export(source, fileName, p1.getX(), p1.getY(), p1.getZ(), p2.getX(), p2.getY(), + p2.getZ()); } private static Block resolveMarkerBlock(String blockId) { @@ -450,30 +310,78 @@ private static Block resolveMarkerBlock(String blockId) { return BuiltInRegistries.BLOCK.get(id).map(holder -> holder.value()).orElse(null); } - private static List findMarkerPositions(ServerLevel level, BlockPos center, Block markerBlock, int radius, - int maxResults) { + private static List findMarkerPositions(ServerLevel level, BlockPos center, Block markerBlock, + int maxRadius, int yTolerance, int maxResults) { List positions = new ArrayList<>(); - int minX = center.getX() - radius; - int maxX = center.getX() + radius; - int minY = center.getY() - radius; - int maxY = center.getY() + radius; - int minZ = center.getZ() - radius; - int maxZ = center.getZ() + radius; BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + int cx = center.getX(); + int cy = center.getY(); + int cz = center.getZ(); + + for (int radius = 0; radius <= maxRadius; radius++) { + int minX = cx - radius; + int maxX = cx + radius; + int minZ = cz - radius; + int maxZ = cz + radius; + + if (radius == 0) { + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, cx, cz, cursor, positions, + maxResults)) { + return positions; + } + continue; + } - for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - cursor.set(x, y, z); - if (level.getBlockState(cursor).getBlock() == markerBlock) { - positions.add(cursor.immutable()); - if (positions.size() >= maxResults) { - return positions; - } - } + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, x, minZ, cursor, positions, + maxResults)) { + return positions; + } + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, x, maxZ, cursor, positions, + maxResults)) { + return positions; + } + } + + for (int z = minZ + 1; z <= maxZ - 1; z++) { + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, minX, z, cursor, positions, + maxResults)) { + return positions; + } + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, maxX, z, cursor, positions, + maxResults)) { + return positions; } } } return positions; } + + private static boolean scanMarkerColumn(ServerLevel level, Block markerBlock, int centerY, int yTolerance, int x, int z, + BlockPos.MutableBlockPos cursor, List positions, int maxResults) { + for (int dy = 0; dy <= yTolerance; dy++) { + int y1 = centerY + dy; + cursor.set(x, y1, z); + if (level.hasChunkAt(cursor) && level.getBlockState(cursor).getBlock() == markerBlock) { + positions.add(cursor.immutable()); + if (positions.size() >= maxResults) { + return true; + } + } + + if (dy == 0) { + continue; + } + + int y2 = centerY - dy; + cursor.set(x, y2, z); + if (level.hasChunkAt(cursor) && level.getBlockState(cursor).getBlock() == markerBlock) { + positions.add(cursor.immutable()); + if (positions.size() >= maxResults) { + return true; + } + } + } + return false; + } } diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json index d91c4a0..37a935c 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/en_us.json @@ -393,7 +393,8 @@ "itemGroup.box3.nature": "Box3:Nature", "itemGroup.box3.structure": "Box3:Structures", "itemGroup.box3.models": "Box3:Models", - "command.box3.box3import.usage": "Usage: /box3import (reads from config/box3/.json),It will generate a building at your current location. ", + "command.box3.box3import.usage": "Usage: /box3import (reads from config/box3/.gz),It will generate a building at your current location. ", + "command.box3.box3export.usage": "Usage: /box3export (place 2 redstone blocks as opposite corners)", "command.box3.box3import.success": "Imported building from config/box3/%s", "command.box3.box3import.progress": "Importing %s... %s%%", "command.box3.box3import.failure": "Import failed: %s", diff --git a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json index 63fe7a4..5f82f63 100644 --- a/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json +++ b/Fabric-1.21.11/src/main/resources/assets/box3/lang/zh_cn.json @@ -381,7 +381,8 @@ "itemGroup.box3.nature": "Box3:自然", "itemGroup.box3.structure": "Box3:建筑", "itemGroup.box3.models": "Box3:模型", - "command.box3.box3import.usage": "用法:/box3import <文件名>(从 config/box3/<文件名>.json 读取),会在你的当前位置生成建筑", + "command.box3.box3import.usage": "用法:/box3import <文件名>(从 config/box3/<文件名>.gz 读取),会在你的当前位置生成建筑", + "command.box3.box3export.usage": "用法:/box3export <文件名>(放置2个红石块作为对角点)", "command.box3.box3import.success": "已从 %s 导入建筑", "command.box3.box3import.progress": "正在导入 %s... %s%%", "command.box3.box3import.failure": "导入失败:%s", From 425b41dcdbadaccc581e07085d5e0985ccff3596 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Sat, 28 Feb 2026 18:22:48 +0800 Subject: [PATCH 3/4] =?UTF-8?q?chore(Fabric-1.21.11):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?mod=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BB=8E1.4.2=E5=88=B01.4.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 升级mod_version属性从1.4.2-mc1.21.11到1.4.3-mc1.21.11, 以反映新的版本发布。 --- Fabric-1.21.11/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fabric-1.21.11/gradle.properties b/Fabric-1.21.11/gradle.properties index 1d62dd8..2cab700 100644 --- a/Fabric-1.21.11/gradle.properties +++ b/Fabric-1.21.11/gradle.properties @@ -12,7 +12,7 @@ loader_version=0.18.4 loom_version=1.15-SNAPSHOT # Mod Properties -mod_version=1.4.2-mc1.21.11 +mod_version=1.4.3-mc1.21.11 maven_group=com.box3lab archives_base_name=box3 From 728dda878669ad6fe5ab8c5eb9f5bb58876cbc14 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Sat, 28 Feb 2026 20:03:20 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(README):=20=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=8C=87=E4=BB=A4=E6=96=87=E6=A1=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 README.md 中新增导出指令文档 - 在 README_en.md 中新增英文版导出指令文档 - 详细说明 /box3export 命令的使用方法 - 解释红石块标记点的自动搜索机制和最大搜索半径 --- README.md | 4 ++++ README_en.md | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index b691001..efbe66e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ 当 `ignoreBarrier = true` 时,跳过屏障方块(不会在世界中放置这些方块)。 - `/box3import ` 当 `ignoreWater = true` 时,所有流体统一替换为空气。 +- **导出指令**: + - `/box3export ` + 自动搜索附近最近的两个 `红石块`(`minecraft:redstone_block`)作为导出区域对角点,并导出到 `config/box3/.gz`。 + - 搜索规则:从近到远扫描,找到两个标记点就停止;最大搜索半径为 `1024`。 ### 🧩 导入神奇代码岛的模型物品 diff --git a/README_en.md b/README_en.md index df3301e..6aa4cb7 100644 --- a/README_en.md +++ b/README_en.md @@ -42,6 +42,15 @@ You can also migrate structures from Box3 directly into your Minecraft world, pr - `/box3import ` When `ignoreWater = true`, all fluids are uniformly replaced with air. +### 📤 Exporting Minecraft Regions to Box3 + +- **Export commands**: + - `/box3export ` + Automatically search for the two nearest `Redstone Block` markers (`minecraft:redstone_block`) around the player, + treat them as opposite corners of the export region, and export that region to `config/box3/.gz`. + - **Search rules**: chunks are scanned from near to far until two marker blocks are found, then the search stops. + The maximum search radius is `1024` blocks. + ### 🧩 Importing Box3 Model Items - **Resource file import**: Supports importing resource packs from the `resourcepacks/` directory.