diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 55c76f4..38fb041 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -30,7 +30,8 @@ "mcp__minecraft-bridge__break_block", "mcp__minecraft-bridge__list_schematics", "mcp__minecraft-bridge__load_schematic", - "mcp__minecraft-bridge__build_schematic" + "mcp__minecraft-bridge__build_schematic", + "mcp__minecraft-bridge__get_nearby_entities" ] } } diff --git a/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc b/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc index 921497e..70901db 100644 Binary files a/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc and b/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc differ diff --git a/minecraft-mcp/src/minecraft_mcp/server.py b/minecraft-mcp/src/minecraft_mcp/server.py index fa3379a..c3f93dc 100644 --- a/minecraft-mcp/src/minecraft_mcp/server.py +++ b/minecraft-mcp/src/minecraft_mcp/server.py @@ -304,12 +304,13 @@ async def teleport(x: float, y: float, z: float) -> str: @mcp.tool() -async def place_block(x: int, y: int, z: int, block_type: str) -> str: +async def place_block(x: int, y: int, z: int, block_type: str, properties: dict | None = None) -> str: """Place a block at the given coordinates. block_type is a Minecraft block ID like 'stone' or 'oak_planks'.""" await _ensure_build_session(x, y, z) - return _fmt(await _request("POST", "/place_block", json={ - "x": x, "y": y, "z": z, "type": block_type, - })) + payload: dict = {"x": x, "y": y, "z": z, "type": block_type} + if properties: + payload["properties"] = properties + return _fmt(await _request("POST", "/place_block", json=payload)) @mcp.tool() @@ -324,14 +325,17 @@ async def place_blocks(blocks: list[dict]) -> str: @mcp.tool() -async def fill(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, block_type: str) -> str: +async def fill(x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, block_type: str, properties: dict | None = None) -> str: """Fill a bounding box with a single block type. Max volume 32x32x32.""" await _ensure_build_session((x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2) - return _fmt(await _request("POST", "/fill", json={ + payload: dict = { "x1": x1, "y1": y1, "z1": z1, "x2": x2, "y2": y2, "z2": z2, "type": block_type, - })) + } + if properties: + payload["properties"] = properties + return _fmt(await _request("POST", "/fill", json=payload)) @mcp.tool() @@ -396,7 +400,8 @@ async def save_schematic(name: str, x1: int, y1: int, z1: int, x2: int, y2: int, async def load_schematic(name: str, x: int, y: int, z: int) -> str: """Load a previously saved schematic and place it at the given origin coordinates. Name may only contain letters, numbers, underscore, and hyphen. - Supports both .litematic (Litematica) and .json formats — .litematic is checked first.""" + Supports .litematic (Litematica), .nbt (Create mod / vanilla structure), and .json formats. + Priority: .litematic → .nbt → .json. Create mod .nbt schematics preserve tile entity data.""" await _ensure_build_session(x, y, z) return _fmt(await _request("POST", "/load_schematic", json={ "name": name, "x": x, "y": y, "z": z, @@ -432,6 +437,7 @@ _SNARKY_75 = [ async def build_schematic(name: str, x: int, y: int, z: int) -> str: """Load a schematic and build it layer by layer (bottom to top) with chat progress. Creates a satisfying visual building effect. Use load_schematic for instant placement instead. + Supports .litematic, .nbt (Create mod / vanilla structure), and .json formats. Name may only contain letters, numbers, underscore, and hyphen.""" # 1. Get schematic info info = await _request("POST", "/get_schematic_info", json={ @@ -524,8 +530,8 @@ async def build_schematic(name: str, x: int, y: int, z: int) -> str: @mcp.tool() async def list_schematics() -> str: - """List all available schematics (.json and .litematic) in the world's schematics folder. - Returns name, format, and file size for each schematic.""" + """List all available schematics (.litematic, .nbt, and .json) in the world's schematics folder. + Returns name, format, and file size for each schematic. Supports Create mod .nbt schematics.""" return _fmt(await _request("GET", "/list_schematics")) diff --git a/src/main/java/com/clod/clod/server/ClodHttpServer.java b/src/main/java/com/clod/clod/server/ClodHttpServer.java index 2b59647..3e99612 100644 --- a/src/main/java/com/clod/clod/server/ClodHttpServer.java +++ b/src/main/java/com/clod/clod/server/ClodHttpServer.java @@ -21,6 +21,7 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.Tag; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; @@ -358,6 +359,19 @@ public class ClodHttpServer { return state; } + private BlockState blockStateWithProperties(Block block, JsonObject properties) { + BlockState state = block.defaultBlockState(); + if (properties != null) { + for (String key : properties.keySet()) { + Property prop = block.getStateDefinition().getProperty(key); + if (prop != null) { + state = setPropertyValue(state, prop, properties.get(key).getAsString()); + } + } + } + return state; + } + private BlockState parseBlockStateFromNBT(CompoundTag entry) { String name = entry.getString("Name"); ResourceLocation blockId = ResourceLocation.parse(name); @@ -392,7 +406,11 @@ public class ClodHttpServer { return (int) (value & mask); } - private record BlockPlacement(BlockPos pos, BlockState state) {} + private record BlockPlacement(BlockPos pos, BlockState state, @javax.annotation.Nullable CompoundTag nbt) { + BlockPlacement(BlockPos pos, BlockState state) { + this(pos, state, null); + } + } private record SchematicData(List placements, String format) {} @@ -478,10 +496,61 @@ public class ClodHttpServer { return placements; } + private List parseNbtSchematic(Path nbtPath, int originX, int originY, int originZ) throws IOException { + List placements = new ArrayList<>(); + CompoundTag root; + try (InputStream is = new BufferedInputStream(Files.newInputStream(nbtPath))) { + root = NbtIo.readCompressed(is, NbtAccounter.unlimitedHeap()); + } + + // Parse palette + ListTag paletteList = root.getList("palette", Tag.TAG_COMPOUND); + BlockState[] palette = new BlockState[paletteList.size()]; + for (int i = 0; i < paletteList.size(); i++) { + palette[i] = parseBlockStateFromNBT(paletteList.getCompound(i)); + } + + // Parse blocks + ListTag blocksList = root.getList("blocks", Tag.TAG_COMPOUND); + for (int i = 0; i < blocksList.size(); i++) { + CompoundTag blockEntry = blocksList.getCompound(i); + + // Position is a ListTag of 3 ints + ListTag posTag = blockEntry.getList("pos", Tag.TAG_INT); + int localX = posTag.getInt(0); + int localY = posTag.getInt(1); + int localZ = posTag.getInt(2); + + int worldX = originX + localX; + int worldY = originY + localY; + int worldZ = originZ + localZ; + + int stateIndex = blockEntry.getInt("state"); + if (stateIndex < 0 || stateIndex >= palette.length) continue; + BlockState state = palette[stateIndex]; + if (state.isAir()) continue; + + // Optional tile entity NBT + CompoundTag nbt = null; + if (blockEntry.contains("nbt", Tag.TAG_COMPOUND)) { + nbt = blockEntry.getCompound("nbt").copy(); + // Update position fields to world coordinates + nbt.putInt("x", worldX); + nbt.putInt("y", worldY); + nbt.putInt("z", worldZ); + } + + placements.add(new BlockPlacement(new BlockPos(worldX, worldY, worldZ), state, nbt)); + } + + return placements; + } + private SchematicData parseSchematic(String name, int originX, int originY, int originZ) throws Exception { Path worldPath = mcServer.getWorldPath(LevelResource.ROOT); Path schematicsDir = worldPath.resolve("schematics"); Path litematicPath = schematicsDir.resolve(name + ".litematic"); + Path nbtPath = schematicsDir.resolve(name + ".nbt"); Path jsonPath = schematicsDir.resolve(name + ".json"); if (Files.exists(litematicPath)) { @@ -499,11 +568,14 @@ public class ClodHttpServer { allPlacements.addAll(parseLitematicRegion(regionNBT, originX, originY, originZ)); } return new SchematicData(allPlacements, "litematic"); + } else if (Files.exists(nbtPath)) { + List placements = parseNbtSchematic(nbtPath, originX, originY, originZ); + return new SchematicData(placements, "nbt"); } else if (Files.exists(jsonPath)) { List placements = parseJsonSchematic(jsonPath, originX, originY, originZ); return new SchematicData(placements, "json"); } else { - throw new java.io.FileNotFoundException("Schematic not found: " + name + " (looked for .litematic and .json)"); + throw new java.io.FileNotFoundException("Schematic not found: " + name + " (looked for .litematic, .nbt, and .json)"); } } @@ -697,10 +769,12 @@ public class ClodHttpServer { return; } + JsonObject props = body.has("properties") ? body.getAsJsonObject("properties") : null; + JsonObject result = runOnServerThread(() -> { ServerLevel level = (ServerLevel) bot.level(); Block block = BuiltInRegistries.BLOCK.get(blockId); - level.setBlock(new BlockPos(x, y, z), block.defaultBlockState(), 3); + level.setBlock(new BlockPos(x, y, z), blockStateWithProperties(block, props), 3); return okJson(); }); sendJson(exchange, 200, result); @@ -747,7 +821,8 @@ public class ClodHttpServer { String type = b.get("type").getAsString(); ResourceLocation blockId = ResourceLocation.parse(type); Block block = BuiltInRegistries.BLOCK.get(blockId); - level.setBlock(new BlockPos(bx, by, bz), block.defaultBlockState(), 3); + JsonObject bProps = b.has("properties") ? b.getAsJsonObject("properties") : null; + level.setBlock(new BlockPos(bx, by, bz), blockStateWithProperties(block, bProps), 3); placed++; } JsonObject json = okJson(); @@ -788,10 +863,12 @@ public class ClodHttpServer { return; } + JsonObject props = body.has("properties") ? body.getAsJsonObject("properties") : null; + JsonObject result = runOnServerThread(() -> { ServerLevel level = (ServerLevel) bot.level(); Block block = BuiltInRegistries.BLOCK.get(blockId); - BlockState state = block.defaultBlockState(); + BlockState state = blockStateWithProperties(block, props); int minX = Math.min(x1, x2), maxX = Math.max(x1, x2); int minY = Math.min(y1, y2), maxY = Math.max(y1, y2); int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2); @@ -1262,6 +1339,13 @@ public class ClodHttpServer { int placed = 0; for (BlockPlacement bp : data.placements()) { level.setBlock(bp.pos(), bp.state(), 2); + if (bp.nbt() != null) { + BlockEntity be = level.getBlockEntity(bp.pos()); + if (be != null) { + be.loadCustomOnly(bp.nbt(), level.registryAccess()); + be.setChanged(); + } + } placed++; } JsonObject json = okJson(); @@ -1403,6 +1487,13 @@ public class ClodHttpServer { int placed = 0; for (BlockPlacement bp : layerPlacements) { level.setBlock(bp.pos(), bp.state(), 2); + if (bp.nbt() != null) { + BlockEntity be = level.getBlockEntity(bp.pos()); + if (be != null) { + be.loadCustomOnly(bp.nbt(), level.registryAccess()); + be.setChanged(); + } + } placed++; } JsonObject json = okJson(); @@ -1430,12 +1521,13 @@ public class ClodHttpServer { try (var stream = Files.list(schematicsDir)) { stream.filter(p -> { String fname = p.getFileName().toString(); - return fname.endsWith(".json") || fname.endsWith(".litematic"); + return fname.endsWith(".json") || fname.endsWith(".litematic") || fname.endsWith(".nbt"); }).sorted().forEach(p -> { JsonObject obj = new JsonObject(); String filename = p.getFileName().toString(); obj.addProperty("name", filename.substring(0, filename.lastIndexOf('.'))); - obj.addProperty("format", filename.endsWith(".litematic") ? "litematic" : "json"); + String format = filename.endsWith(".litematic") ? "litematic" : filename.endsWith(".nbt") ? "nbt" : "json"; + obj.addProperty("format", format); try { obj.addProperty("size_bytes", Files.size(p)); } catch (IOException e) {