feat: create mod support

This commit is contained in:
2026-02-15 19:36:55 +01:00
parent 79665a0acc
commit 9076782d54
4 changed files with 117 additions and 18 deletions
+2 -1
View File
@@ -30,7 +30,8 @@
"mcp__minecraft-bridge__break_block", "mcp__minecraft-bridge__break_block",
"mcp__minecraft-bridge__list_schematics", "mcp__minecraft-bridge__list_schematics",
"mcp__minecraft-bridge__load_schematic", "mcp__minecraft-bridge__load_schematic",
"mcp__minecraft-bridge__build_schematic" "mcp__minecraft-bridge__build_schematic",
"mcp__minecraft-bridge__get_nearby_entities"
] ]
} }
} }
+16 -10
View File
@@ -304,12 +304,13 @@ async def teleport(x: float, y: float, z: float) -> str:
@mcp.tool() @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'.""" """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) await _ensure_build_session(x, y, z)
return _fmt(await _request("POST", "/place_block", json={ payload: dict = {"x": x, "y": y, "z": z, "type": block_type}
"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() @mcp.tool()
@@ -324,14 +325,17 @@ async def place_blocks(blocks: list[dict]) -> str:
@mcp.tool() @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.""" """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) 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, "x1": x1, "y1": y1, "z1": z1,
"x2": x2, "y2": y2, "z2": z2, "x2": x2, "y2": y2, "z2": z2,
"type": block_type, "type": block_type,
})) }
if properties:
payload["properties"] = properties
return _fmt(await _request("POST", "/fill", json=payload))
@mcp.tool() @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: 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. """Load a previously saved schematic and place it at the given origin coordinates.
Name may only contain letters, numbers, underscore, and hyphen. 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) await _ensure_build_session(x, y, z)
return _fmt(await _request("POST", "/load_schematic", json={ return _fmt(await _request("POST", "/load_schematic", json={
"name": name, "x": x, "y": y, "z": z, "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: 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. """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. 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.""" Name may only contain letters, numbers, underscore, and hyphen."""
# 1. Get schematic info # 1. Get schematic info
info = await _request("POST", "/get_schematic_info", json={ 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() @mcp.tool()
async def list_schematics() -> str: async def list_schematics() -> str:
"""List all available schematics (.json and .litematic) in the world's schematics folder. """List all available schematics (.litematic, .nbt, and .json) in the world's schematics folder.
Returns name, format, and file size for each schematic.""" Returns name, format, and file size for each schematic. Supports Create mod .nbt schematics."""
return _fmt(await _request("GET", "/list_schematics")) return _fmt(await _request("GET", "/list_schematics"))
@@ -21,6 +21,7 @@ import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.Tag; 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.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
@@ -358,6 +359,19 @@ public class ClodHttpServer {
return state; 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) { private BlockState parseBlockStateFromNBT(CompoundTag entry) {
String name = entry.getString("Name"); String name = entry.getString("Name");
ResourceLocation blockId = ResourceLocation.parse(name); ResourceLocation blockId = ResourceLocation.parse(name);
@@ -392,7 +406,11 @@ public class ClodHttpServer {
return (int) (value & mask); 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<BlockPlacement> placements, String format) {} private record SchematicData(List<BlockPlacement> placements, String format) {}
@@ -478,10 +496,61 @@ public class ClodHttpServer {
return placements; return placements;
} }
private List<BlockPlacement> parseNbtSchematic(Path nbtPath, int originX, int originY, int originZ) throws IOException {
List<BlockPlacement> 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 { private SchematicData parseSchematic(String name, int originX, int originY, int originZ) throws Exception {
Path worldPath = mcServer.getWorldPath(LevelResource.ROOT); Path worldPath = mcServer.getWorldPath(LevelResource.ROOT);
Path schematicsDir = worldPath.resolve("schematics"); Path schematicsDir = worldPath.resolve("schematics");
Path litematicPath = schematicsDir.resolve(name + ".litematic"); Path litematicPath = schematicsDir.resolve(name + ".litematic");
Path nbtPath = schematicsDir.resolve(name + ".nbt");
Path jsonPath = schematicsDir.resolve(name + ".json"); Path jsonPath = schematicsDir.resolve(name + ".json");
if (Files.exists(litematicPath)) { if (Files.exists(litematicPath)) {
@@ -499,11 +568,14 @@ public class ClodHttpServer {
allPlacements.addAll(parseLitematicRegion(regionNBT, originX, originY, originZ)); allPlacements.addAll(parseLitematicRegion(regionNBT, originX, originY, originZ));
} }
return new SchematicData(allPlacements, "litematic"); return new SchematicData(allPlacements, "litematic");
} else if (Files.exists(nbtPath)) {
List<BlockPlacement> placements = parseNbtSchematic(nbtPath, originX, originY, originZ);
return new SchematicData(placements, "nbt");
} else if (Files.exists(jsonPath)) { } else if (Files.exists(jsonPath)) {
List<BlockPlacement> placements = parseJsonSchematic(jsonPath, originX, originY, originZ); List<BlockPlacement> placements = parseJsonSchematic(jsonPath, originX, originY, originZ);
return new SchematicData(placements, "json"); return new SchematicData(placements, "json");
} else { } 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; return;
} }
JsonObject props = body.has("properties") ? body.getAsJsonObject("properties") : null;
JsonObject result = runOnServerThread(() -> { JsonObject result = runOnServerThread(() -> {
ServerLevel level = (ServerLevel) bot.level(); ServerLevel level = (ServerLevel) bot.level();
Block block = BuiltInRegistries.BLOCK.get(blockId); 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(); return okJson();
}); });
sendJson(exchange, 200, result); sendJson(exchange, 200, result);
@@ -747,7 +821,8 @@ public class ClodHttpServer {
String type = b.get("type").getAsString(); String type = b.get("type").getAsString();
ResourceLocation blockId = ResourceLocation.parse(type); ResourceLocation blockId = ResourceLocation.parse(type);
Block block = BuiltInRegistries.BLOCK.get(blockId); 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++; placed++;
} }
JsonObject json = okJson(); JsonObject json = okJson();
@@ -788,10 +863,12 @@ public class ClodHttpServer {
return; return;
} }
JsonObject props = body.has("properties") ? body.getAsJsonObject("properties") : null;
JsonObject result = runOnServerThread(() -> { JsonObject result = runOnServerThread(() -> {
ServerLevel level = (ServerLevel) bot.level(); ServerLevel level = (ServerLevel) bot.level();
Block block = BuiltInRegistries.BLOCK.get(blockId); 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 minX = Math.min(x1, x2), maxX = Math.max(x1, x2);
int minY = Math.min(y1, y2), maxY = Math.max(y1, y2); int minY = Math.min(y1, y2), maxY = Math.max(y1, y2);
int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2); int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2);
@@ -1262,6 +1339,13 @@ public class ClodHttpServer {
int placed = 0; int placed = 0;
for (BlockPlacement bp : data.placements()) { for (BlockPlacement bp : data.placements()) {
level.setBlock(bp.pos(), bp.state(), 2); 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++; placed++;
} }
JsonObject json = okJson(); JsonObject json = okJson();
@@ -1403,6 +1487,13 @@ public class ClodHttpServer {
int placed = 0; int placed = 0;
for (BlockPlacement bp : layerPlacements) { for (BlockPlacement bp : layerPlacements) {
level.setBlock(bp.pos(), bp.state(), 2); 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++; placed++;
} }
JsonObject json = okJson(); JsonObject json = okJson();
@@ -1430,12 +1521,13 @@ public class ClodHttpServer {
try (var stream = Files.list(schematicsDir)) { try (var stream = Files.list(schematicsDir)) {
stream.filter(p -> { stream.filter(p -> {
String fname = p.getFileName().toString(); 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 -> { }).sorted().forEach(p -> {
JsonObject obj = new JsonObject(); JsonObject obj = new JsonObject();
String filename = p.getFileName().toString(); String filename = p.getFileName().toString();
obj.addProperty("name", filename.substring(0, filename.lastIndexOf('.'))); 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 { try {
obj.addProperty("size_bytes", Files.size(p)); obj.addProperty("size_bytes", Files.size(p));
} catch (IOException e) { } catch (IOException e) {