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 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..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 @@ -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,21 @@ 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 MAX_MARKER_SCAN_RADIUS = 1024; + private static final int MARKER_Y_TOLERANCE = 512; + private static final SuggestionProvider BOX3_FILE_SUGGESTIONS = (context, builder) -> { try { List files = Box3ImportFiles.listJsonFiles(); @@ -125,6 +134,16 @@ public static void register() { .then(literal("toggle") .executes(context -> toggleBarrierVisible( context.getSource()))))); + + dispatcher.register( + literal("box3export") + .executes(context -> showBox3ExportUsage(context.getSource())) + .then(argument("fileName", StringArgumentType.word()) + .executes(context -> executeBox3ExportByMarkers( + context.getSource(), + StringArgumentType.getString( + context, + "fileName"))))); }); } @@ -158,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()); @@ -221,4 +245,143 @@ 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 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(DEFAULT_EXPORT_MARKER_BLOCK); + if (markerBlock == null) { + source.sendFailure(Component.translatable("command.box3.box3export.marker_invalid", + DEFAULT_EXPORT_MARKER_BLOCK)); + return 0; + } + + List positions = findMarkerPositions(source.getLevel(), player.blockPosition(), markerBlock, + MAX_MARKER_SCAN_RADIUS, MARKER_Y_TOLERANCE, 2); + if (positions.size() < 2) { + source.sendFailure(Component.translatable( + "command.box3.box3export.marker_count_invalid", + MAX_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 maxRadius, int yTolerance, int maxResults) { + List positions = new ArrayList<>(); + 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 x = minX; x <= maxX; x++) { + 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/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..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", @@ -411,6 +412,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..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", @@ -399,6 +400,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": "缩放", 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.