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__list_schematics",
"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()
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"))
@@ -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<BlockPlacement> placements, String format) {}
@@ -478,10 +496,61 @@ public class ClodHttpServer {
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 {
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<BlockPlacement> placements = parseNbtSchematic(nbtPath, originX, originY, originZ);
return new SchematicData(placements, "nbt");
} else if (Files.exists(jsonPath)) {
List<BlockPlacement> 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) {