feat: create mod support
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user