feat: clod can build from litematica schematics
This commit is contained in:
@@ -26,7 +26,10 @@
|
||||
"mcp__minecraft-bridge__fill",
|
||||
"mcp__minecraft-bridge__teleport",
|
||||
"mcp__minecraft-bridge__save_schematic",
|
||||
"mcp__minecraft-bridge__get_biome"
|
||||
"mcp__minecraft-bridge__get_biome",
|
||||
"mcp__minecraft-bridge__break_block",
|
||||
"mcp__minecraft-bridge__list_schematics",
|
||||
"mcp__minecraft-bridge__load_schematic"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -315,11 +315,19 @@ async def save_schematic(name: str, x1: int, y1: int, z1: int, x2: int, y2: int,
|
||||
@mcp.tool()
|
||||
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."""
|
||||
Name may only contain letters, numbers, underscore, and hyphen.
|
||||
Supports both .litematic (Litematica) and .json formats — .litematic is checked first."""
|
||||
await _ensure_build_session(x, y, z)
|
||||
return _fmt(await _request("POST", "/load_schematic", json={
|
||||
"name": name, "x": x, "y": y, "z": z,
|
||||
}, timeout=30.0))
|
||||
}, timeout=120.0))
|
||||
|
||||
|
||||
@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."""
|
||||
return _fmt(await _request("GET", "/list_schematics"))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
@@ -16,6 +16,11 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
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.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -26,17 +31,21 @@ import com.google.gson.GsonBuilder;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
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.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -87,6 +96,7 @@ public class ClodHttpServer {
|
||||
httpServer.createContext("/clear_area", this::handleClearArea);
|
||||
httpServer.createContext("/save_schematic", this::handleSaveSchematic);
|
||||
httpServer.createContext("/load_schematic", this::handleLoadSchematic);
|
||||
httpServer.createContext("/list_schematics", this::handleListSchematics);
|
||||
|
||||
// Long-poll endpoint
|
||||
httpServer.createContext("/chat/wait", this::handleChatWait);
|
||||
@@ -126,6 +136,18 @@ public class ClodHttpServer {
|
||||
return future.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private <T> T runOnServerThreadLong(Supplier<T> task) throws Exception {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
mcServer.execute(() -> {
|
||||
try {
|
||||
future.complete(task.get());
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future.get(60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private void sendJson(HttpExchange exchange, int status, JsonObject json) throws IOException {
|
||||
@@ -323,6 +345,99 @@ public class ClodHttpServer {
|
||||
return prop.getName(state.getValue(prop));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends Comparable<T>> BlockState setPropertyValue(BlockState state, Property<T> prop, String value) {
|
||||
Optional<T> optVal = prop.getValue(value);
|
||||
if (optVal.isPresent()) {
|
||||
return state.setValue(prop, optVal.get());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private BlockState parseBlockStateFromNBT(CompoundTag entry) {
|
||||
String name = entry.getString("Name");
|
||||
ResourceLocation blockId = ResourceLocation.parse(name);
|
||||
if (!BuiltInRegistries.BLOCK.containsKey(blockId)) {
|
||||
LOGGER.warn("Unknown block in litematic: {}, using air", name);
|
||||
return Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
Block block = BuiltInRegistries.BLOCK.get(blockId);
|
||||
BlockState state = block.defaultBlockState();
|
||||
if (entry.contains("Properties", Tag.TAG_COMPOUND)) {
|
||||
CompoundTag props = entry.getCompound("Properties");
|
||||
for (String key : props.getAllKeys()) {
|
||||
String val = props.getString(key);
|
||||
Property<?> prop = block.getStateDefinition().getProperty(key);
|
||||
if (prop != null) {
|
||||
state = setPropertyValue(state, prop, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static int getFromBitArray(long[] array, int index, int bitsPerEntry) {
|
||||
long mask = (1L << bitsPerEntry) - 1;
|
||||
long bitIndex = (long) index * bitsPerEntry;
|
||||
int startLong = (int) (bitIndex >>> 6);
|
||||
int startBit = (int) (bitIndex & 63);
|
||||
long value = array[startLong] >>> startBit;
|
||||
if (startBit + bitsPerEntry > 64 && startLong + 1 < array.length) {
|
||||
value |= array[startLong + 1] << (64 - startBit);
|
||||
}
|
||||
return (int) (value & mask);
|
||||
}
|
||||
|
||||
private record BlockPlacement(BlockPos pos, BlockState state) {}
|
||||
|
||||
private List<BlockPlacement> parseLitematicRegion(CompoundTag regionNBT, int originX, int originY, int originZ) {
|
||||
List<BlockPlacement> placements = new ArrayList<>();
|
||||
|
||||
CompoundTag posTag = regionNBT.getCompound("Position");
|
||||
int posX = posTag.getInt("x");
|
||||
int posY = posTag.getInt("y");
|
||||
int posZ = posTag.getInt("z");
|
||||
|
||||
CompoundTag sizeTag = regionNBT.getCompound("Size");
|
||||
int sizeX = sizeTag.getInt("x");
|
||||
int sizeY = sizeTag.getInt("y");
|
||||
int sizeZ = sizeTag.getInt("z");
|
||||
|
||||
int absX = Math.abs(sizeX);
|
||||
int absY = Math.abs(sizeY);
|
||||
int absZ = Math.abs(sizeZ);
|
||||
|
||||
ListTag paletteList = regionNBT.getList("BlockStatePalette", Tag.TAG_COMPOUND);
|
||||
BlockState[] palette = new BlockState[paletteList.size()];
|
||||
for (int i = 0; i < paletteList.size(); i++) {
|
||||
palette[i] = parseBlockStateFromNBT(paletteList.getCompound(i));
|
||||
}
|
||||
|
||||
int bitsPerEntry = Math.max(2, (int) Math.ceil(Math.log(palette.length) / Math.log(2)));
|
||||
|
||||
long[] blockStates = regionNBT.getLongArray("BlockStates");
|
||||
int volume = absX * absY * absZ;
|
||||
|
||||
for (int i = 0; i < volume; i++) {
|
||||
int paletteIndex = getFromBitArray(blockStates, i, bitsPerEntry);
|
||||
if (paletteIndex < 0 || paletteIndex >= palette.length) continue;
|
||||
BlockState state = palette[paletteIndex];
|
||||
if (state.isAir()) continue;
|
||||
|
||||
int localY = i / (absX * absZ);
|
||||
int localZ = (i % (absX * absZ)) / absX;
|
||||
int localX = i % absX;
|
||||
|
||||
int worldX = originX + posX + (sizeX < 0 ? localX + sizeX + 1 : localX);
|
||||
int worldY = originY + posY + (sizeY < 0 ? localY + sizeY + 1 : localY);
|
||||
int worldZ = originZ + posZ + (sizeZ < 0 ? localZ + sizeZ + 1 : localZ);
|
||||
|
||||
placements.add(new BlockPlacement(new BlockPos(worldX, worldY, worldZ), state));
|
||||
}
|
||||
|
||||
return placements;
|
||||
}
|
||||
|
||||
private void handleNearbyEntities(HttpExchange exchange) throws IOException {
|
||||
if (!requireGet(exchange)) return;
|
||||
try {
|
||||
@@ -965,6 +1080,13 @@ public class ClodHttpServer {
|
||||
blockObj.addProperty("y", y - minY);
|
||||
blockObj.addProperty("z", z - minZ);
|
||||
blockObj.addProperty("type", BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
|
||||
if (!state.getProperties().isEmpty()) {
|
||||
JsonObject props = new JsonObject();
|
||||
for (Property<?> prop : state.getProperties()) {
|
||||
props.addProperty(prop.getName(), getPropertyValueString(state, prop));
|
||||
}
|
||||
blockObj.add("properties", props);
|
||||
}
|
||||
arr.add(blockObj);
|
||||
}
|
||||
}
|
||||
@@ -974,7 +1096,7 @@ public class ClodHttpServer {
|
||||
|
||||
// Build schematic JSON
|
||||
JsonObject schematic = new JsonObject();
|
||||
schematic.addProperty("version", 1);
|
||||
schematic.addProperty("version", 2);
|
||||
JsonObject size = new JsonObject();
|
||||
size.addProperty("x", maxX - minX + 1);
|
||||
size.addProperty("y", maxY - minY + 1);
|
||||
@@ -1023,59 +1145,151 @@ public class ClodHttpServer {
|
||||
}
|
||||
|
||||
Path worldPath = mcServer.getWorldPath(LevelResource.ROOT);
|
||||
Path filePath = worldPath.resolve("schematics").resolve(name + ".json");
|
||||
Path schematicsDir = worldPath.resolve("schematics");
|
||||
Path litematicPath = schematicsDir.resolve(name + ".litematic");
|
||||
Path jsonPath = schematicsDir.resolve(name + ".json");
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
sendJson(exchange, 404, errorJson("Schematic not found: " + name));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and parse on HTTP thread
|
||||
JsonObject schematic;
|
||||
try (FileReader reader = new FileReader(filePath.toFile(), StandardCharsets.UTF_8)) {
|
||||
schematic = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
}
|
||||
|
||||
JsonArray blocksArr = schematic.getAsJsonArray("blocks");
|
||||
|
||||
// Validate all block types before placing
|
||||
for (int i = 0; i < blocksArr.size(); i++) {
|
||||
JsonObject b = blocksArr.get(i).getAsJsonObject();
|
||||
String type = b.get("type").getAsString();
|
||||
ResourceLocation blockId = ResourceLocation.parse(type);
|
||||
if (!BuiltInRegistries.BLOCK.containsKey(blockId)) {
|
||||
sendJson(exchange, 400, errorJson("Invalid block type in schematic: " + type));
|
||||
if (Files.exists(litematicPath)) {
|
||||
// --- Litematic format ---
|
||||
CompoundTag root;
|
||||
try (InputStream is = new BufferedInputStream(Files.newInputStream(litematicPath))) {
|
||||
root = NbtIo.readCompressed(is, NbtAccounter.unlimitedHeap());
|
||||
} catch (Exception e) {
|
||||
sendJson(exchange, 400, errorJson("Failed to read litematic file: " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Place blocks on server thread
|
||||
JsonObject result = runOnServerThread(() -> {
|
||||
ServerLevel level = (ServerLevel) bot.level();
|
||||
int placed = 0;
|
||||
if (!root.contains("Regions", Tag.TAG_COMPOUND)) {
|
||||
sendJson(exchange, 400, errorJson("Invalid litematic file: no Regions compound found"));
|
||||
return;
|
||||
}
|
||||
|
||||
CompoundTag regions = root.getCompound("Regions");
|
||||
List<BlockPlacement> allPlacements = new ArrayList<>();
|
||||
for (String regionName : regions.getAllKeys()) {
|
||||
CompoundTag regionNBT = regions.getCompound(regionName);
|
||||
allPlacements.addAll(parseLitematicRegion(regionNBT, originX, originY, originZ));
|
||||
}
|
||||
|
||||
if (allPlacements.isEmpty()) {
|
||||
JsonObject result = okJson();
|
||||
result.addProperty("placed", 0);
|
||||
result.addProperty("format", "litematic");
|
||||
sendJson(exchange, 200, result);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject result = runOnServerThreadLong(() -> {
|
||||
ServerLevel level = (ServerLevel) bot.level();
|
||||
int placed = 0;
|
||||
for (BlockPlacement bp : allPlacements) {
|
||||
level.setBlock(bp.pos(), bp.state(), 2);
|
||||
placed++;
|
||||
}
|
||||
JsonObject json = okJson();
|
||||
json.addProperty("placed", placed);
|
||||
json.addProperty("format", "litematic");
|
||||
return json;
|
||||
});
|
||||
sendJson(exchange, 200, result);
|
||||
|
||||
} else if (Files.exists(jsonPath)) {
|
||||
// --- JSON format ---
|
||||
JsonObject schematic;
|
||||
try (FileReader reader = new FileReader(jsonPath.toFile(), StandardCharsets.UTF_8)) {
|
||||
schematic = JsonParser.parseReader(reader).getAsJsonObject();
|
||||
}
|
||||
|
||||
JsonArray blocksArr = schematic.getAsJsonArray("blocks");
|
||||
|
||||
// Validate all block types before placing
|
||||
for (int i = 0; i < blocksArr.size(); i++) {
|
||||
JsonObject b = blocksArr.get(i).getAsJsonObject();
|
||||
int bx = b.get("x").getAsInt() + originX;
|
||||
int by = b.get("y").getAsInt() + originY;
|
||||
int bz = b.get("z").getAsInt() + originZ;
|
||||
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);
|
||||
placed++;
|
||||
if (!BuiltInRegistries.BLOCK.containsKey(blockId)) {
|
||||
sendJson(exchange, 400, errorJson("Invalid block type in schematic: " + type));
|
||||
return;
|
||||
}
|
||||
}
|
||||
JsonObject json = okJson();
|
||||
json.addProperty("placed", placed);
|
||||
return json;
|
||||
});
|
||||
sendJson(exchange, 200, result);
|
||||
|
||||
// Place blocks on server thread
|
||||
JsonObject result = runOnServerThreadLong(() -> {
|
||||
ServerLevel level = (ServerLevel) bot.level();
|
||||
int placed = 0;
|
||||
for (int i = 0; i < blocksArr.size(); i++) {
|
||||
JsonObject b = blocksArr.get(i).getAsJsonObject();
|
||||
int bx = b.get("x").getAsInt() + originX;
|
||||
int by = b.get("y").getAsInt() + originY;
|
||||
int bz = b.get("z").getAsInt() + originZ;
|
||||
String type = b.get("type").getAsString();
|
||||
ResourceLocation blockId = ResourceLocation.parse(type);
|
||||
Block block = BuiltInRegistries.BLOCK.get(blockId);
|
||||
BlockState state = block.defaultBlockState();
|
||||
if (b.has("properties")) {
|
||||
JsonObject props = b.getAsJsonObject("properties");
|
||||
for (String key : props.keySet()) {
|
||||
Property<?> prop = block.getStateDefinition().getProperty(key);
|
||||
if (prop != null) {
|
||||
state = setPropertyValue(state, prop, props.get(key).getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
level.setBlock(new BlockPos(bx, by, bz), state, 2);
|
||||
placed++;
|
||||
}
|
||||
JsonObject json = okJson();
|
||||
json.addProperty("placed", placed);
|
||||
json.addProperty("format", "json");
|
||||
return json;
|
||||
});
|
||||
sendJson(exchange, 200, result);
|
||||
|
||||
} else {
|
||||
sendJson(exchange, 404, errorJson("Schematic not found: " + name + " (looked for .litematic and .json)"));
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
sendJson(exchange, 504, errorJson("Server thread timeout"));
|
||||
sendJson(exchange, 504, errorJson("Server thread timeout (schematic may be too large)"));
|
||||
} catch (Exception e) {
|
||||
sendJson(exchange, 400, errorJson(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleListSchematics(HttpExchange exchange) throws IOException {
|
||||
if (!requireGet(exchange)) return;
|
||||
try {
|
||||
Path worldPath = mcServer.getWorldPath(LevelResource.ROOT);
|
||||
Path schematicsDir = worldPath.resolve("schematics");
|
||||
|
||||
JsonArray arr = new JsonArray();
|
||||
if (Files.exists(schematicsDir)) {
|
||||
try (var stream = Files.list(schematicsDir)) {
|
||||
stream.filter(p -> {
|
||||
String fname = p.getFileName().toString();
|
||||
return fname.endsWith(".json") || fname.endsWith(".litematic");
|
||||
}).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");
|
||||
try {
|
||||
obj.addProperty("size_bytes", Files.size(p));
|
||||
} catch (IOException e) {
|
||||
obj.addProperty("size_bytes", -1);
|
||||
}
|
||||
arr.add(obj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject result = okJson();
|
||||
result.add("schematics", arr);
|
||||
sendJson(exchange, 200, result);
|
||||
} catch (Exception e) {
|
||||
sendJson(exchange, 500, errorJson(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Long-poll ---
|
||||
|
||||
private void handleChatWait(HttpExchange exchange) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user