chore: add snarky remarks while building from a schematic
This commit is contained in:
@@ -29,7 +29,8 @@
|
|||||||
"mcp__minecraft-bridge__get_biome",
|
"mcp__minecraft-bridge__get_biome",
|
||||||
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -323,6 +324,122 @@ async def load_schematic(name: str, x: int, y: int, z: int) -> str:
|
|||||||
}, timeout=120.0))
|
}, timeout=120.0))
|
||||||
|
|
||||||
|
|
||||||
|
_SNARKY_25 = [
|
||||||
|
"Quarter done with {name}... whoever designed this owes me an explanation.",
|
||||||
|
"25% into {name}. Starting to question my life choices.",
|
||||||
|
"A quarter of {name} is done. It's giving... abstract art.",
|
||||||
|
"25% through {name}. I deserve a raise for this.",
|
||||||
|
"One quarter of {name} complete. Three more quarters of suffering to go.",
|
||||||
|
]
|
||||||
|
|
||||||
|
_SNARKY_50 = [
|
||||||
|
"Halfway through {name}. It's... starting to look like something, I guess.",
|
||||||
|
"50% of {name} done. No turning back now.",
|
||||||
|
"Half of {name} built. The other half is judging me.",
|
||||||
|
"Halfway done with {name}. Send snacks.",
|
||||||
|
"50% into {name}. I've seen worse. Not much worse, but worse.",
|
||||||
|
]
|
||||||
|
|
||||||
|
_SNARKY_75 = [
|
||||||
|
"75% done with {name}. I can almost see the finish line... and it's ugly.",
|
||||||
|
"Almost done with {name}. My masterpiece. And by masterpiece, I mean your fault.",
|
||||||
|
"Three quarters through {name}. I'm basically an artist now.",
|
||||||
|
"75% of {name} complete. I'm starting to feel things. Mostly exhaustion.",
|
||||||
|
"Nearly there with {name}. Don't look at it yet. Actually, don't look at it ever.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
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.
|
||||||
|
Name may only contain letters, numbers, underscore, and hyphen."""
|
||||||
|
# 1. Get schematic info
|
||||||
|
info = await _request("POST", "/get_schematic_info", json={
|
||||||
|
"name": name, "x": x, "y": y, "z": z,
|
||||||
|
}, timeout=15.0)
|
||||||
|
|
||||||
|
if isinstance(info, dict) and "error" in info:
|
||||||
|
return _fmt(info)
|
||||||
|
|
||||||
|
total_blocks = info.get("total_blocks", 0)
|
||||||
|
layers = info.get("layers", [])
|
||||||
|
fmt = info.get("format", "unknown")
|
||||||
|
num_layers = len(layers)
|
||||||
|
|
||||||
|
if num_layers == 0:
|
||||||
|
return _fmt({"ok": True, "message": "Schematic is empty, nothing to build.", "placed": 0})
|
||||||
|
|
||||||
|
# 2. Start build session and announce
|
||||||
|
await _ensure_build_session(x, y, z)
|
||||||
|
await _request("POST", "/chat", json={
|
||||||
|
"message": f"Starting build of {name}! {total_blocks} blocks, {num_layers} layers."
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Build layer by layer
|
||||||
|
total_placed = 0
|
||||||
|
milestones_hit: set[int] = set()
|
||||||
|
milestone_remarks = {25: _SNARKY_25, 50: _SNARKY_50, 75: _SNARKY_75}
|
||||||
|
for i, layer in enumerate(layers):
|
||||||
|
layer_y = layer["y"]
|
||||||
|
block_count = layer["block_count"]
|
||||||
|
|
||||||
|
# Keep build session alive and look at current layer
|
||||||
|
await _ensure_build_session(x, layer_y, z)
|
||||||
|
|
||||||
|
# Check if this layer crosses a milestone threshold
|
||||||
|
progress = (i + 1) / num_layers
|
||||||
|
remark = None
|
||||||
|
for threshold, pool in milestone_remarks.items():
|
||||||
|
if threshold not in milestones_hit and progress >= threshold / 100:
|
||||||
|
milestones_hit.add(threshold)
|
||||||
|
remark = random.choice(pool).format(name=name)
|
||||||
|
break
|
||||||
|
|
||||||
|
if remark:
|
||||||
|
await _request("POST", "/chat", json={"message": remark})
|
||||||
|
else:
|
||||||
|
await _request("POST", "/chat", json={
|
||||||
|
"message": f"Layer {i + 1}/{num_layers} (Y={layer_y}) - {block_count} blocks..."
|
||||||
|
})
|
||||||
|
|
||||||
|
result = await _request("POST", "/place_schematic_layer", json={
|
||||||
|
"name": name, "x": x, "y": y, "z": z, "layer_y": layer_y,
|
||||||
|
}, timeout=15.0)
|
||||||
|
|
||||||
|
if isinstance(result, dict) and "error" in result:
|
||||||
|
await _request("POST", "/chat", json={
|
||||||
|
"message": f"Build failed at layer {i + 1}/{num_layers}! Error: {result['error']}"
|
||||||
|
})
|
||||||
|
return _fmt({
|
||||||
|
"ok": False,
|
||||||
|
"error": result["error"],
|
||||||
|
"layers_completed": i,
|
||||||
|
"layers_total": num_layers,
|
||||||
|
"blocks_placed": total_placed,
|
||||||
|
})
|
||||||
|
|
||||||
|
placed = result.get("placed", 0)
|
||||||
|
total_placed += placed
|
||||||
|
|
||||||
|
# Visual delay between layers
|
||||||
|
if i < num_layers - 1:
|
||||||
|
await asyncio.sleep(0.4)
|
||||||
|
|
||||||
|
# 4. Announce completion
|
||||||
|
await _request("POST", "/chat", json={
|
||||||
|
"message": f"Build complete! Placed {total_placed} blocks in {num_layers} layers."
|
||||||
|
})
|
||||||
|
|
||||||
|
return _fmt({
|
||||||
|
"ok": True,
|
||||||
|
"name": name,
|
||||||
|
"format": fmt,
|
||||||
|
"layers": num_layers,
|
||||||
|
"total_placed": total_placed,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@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 (.json and .litematic) in the world's schematics folder.
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ public class ClodHttpServer {
|
|||||||
httpServer.createContext("/clear_area", this::handleClearArea);
|
httpServer.createContext("/clear_area", this::handleClearArea);
|
||||||
httpServer.createContext("/save_schematic", this::handleSaveSchematic);
|
httpServer.createContext("/save_schematic", this::handleSaveSchematic);
|
||||||
httpServer.createContext("/load_schematic", this::handleLoadSchematic);
|
httpServer.createContext("/load_schematic", this::handleLoadSchematic);
|
||||||
|
httpServer.createContext("/get_schematic_info", this::handleGetSchematicInfo);
|
||||||
|
httpServer.createContext("/place_schematic_layer", this::handlePlaceSchematicLayer);
|
||||||
httpServer.createContext("/list_schematics", this::handleListSchematics);
|
httpServer.createContext("/list_schematics", this::handleListSchematics);
|
||||||
|
|
||||||
// Long-poll endpoint
|
// Long-poll endpoint
|
||||||
@@ -390,6 +392,8 @@ public class ClodHttpServer {
|
|||||||
|
|
||||||
private record BlockPlacement(BlockPos pos, BlockState state) {}
|
private record BlockPlacement(BlockPos pos, BlockState state) {}
|
||||||
|
|
||||||
|
private record SchematicData(List<BlockPlacement> placements, String format) {}
|
||||||
|
|
||||||
private List<BlockPlacement> parseLitematicRegion(CompoundTag regionNBT, int originX, int originY, int originZ) {
|
private List<BlockPlacement> parseLitematicRegion(CompoundTag regionNBT, int originX, int originY, int originZ) {
|
||||||
List<BlockPlacement> placements = new ArrayList<>();
|
List<BlockPlacement> placements = new ArrayList<>();
|
||||||
|
|
||||||
@@ -438,6 +442,69 @@ public class ClodHttpServer {
|
|||||||
return placements;
|
return placements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<BlockPlacement> parseJsonSchematic(Path jsonPath, int originX, int originY, int originZ) throws IOException {
|
||||||
|
List<BlockPlacement> placements = new ArrayList<>();
|
||||||
|
JsonObject schematic;
|
||||||
|
try (FileReader reader = new FileReader(jsonPath.toFile(), StandardCharsets.UTF_8)) {
|
||||||
|
schematic = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
JsonArray blocksArr = schematic.getAsJsonArray("blocks");
|
||||||
|
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);
|
||||||
|
if (!BuiltInRegistries.BLOCK.containsKey(blockId)) {
|
||||||
|
LOGGER.warn("Unknown block in JSON schematic: {}, skipping", type);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
placements.add(new BlockPlacement(new BlockPos(bx, by, bz), state));
|
||||||
|
}
|
||||||
|
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 jsonPath = schematicsDir.resolve(name + ".json");
|
||||||
|
|
||||||
|
if (Files.exists(litematicPath)) {
|
||||||
|
CompoundTag root;
|
||||||
|
try (InputStream is = new BufferedInputStream(Files.newInputStream(litematicPath))) {
|
||||||
|
root = NbtIo.readCompressed(is, NbtAccounter.unlimitedHeap());
|
||||||
|
}
|
||||||
|
if (!root.contains("Regions", Tag.TAG_COMPOUND)) {
|
||||||
|
throw new IllegalArgumentException("Invalid litematic file: no Regions compound found");
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
return new SchematicData(allPlacements, "litematic");
|
||||||
|
} 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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleNearbyEntities(HttpExchange exchange) throws IOException {
|
private void handleNearbyEntities(HttpExchange exchange) throws IOException {
|
||||||
if (!requireGet(exchange)) return;
|
if (!requireGet(exchange)) return;
|
||||||
try {
|
try {
|
||||||
@@ -1144,110 +1211,38 @@ public class ClodHttpServer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path worldPath = mcServer.getWorldPath(LevelResource.ROOT);
|
SchematicData data;
|
||||||
Path schematicsDir = worldPath.resolve("schematics");
|
try {
|
||||||
Path litematicPath = schematicsDir.resolve(name + ".litematic");
|
data = parseSchematic(name, originX, originY, originZ);
|
||||||
Path jsonPath = schematicsDir.resolve(name + ".json");
|
} catch (java.io.FileNotFoundException e) {
|
||||||
|
sendJson(exchange, 404, errorJson(e.getMessage()));
|
||||||
if (Files.exists(litematicPath)) {
|
return;
|
||||||
// --- Litematic format ---
|
} catch (IllegalArgumentException e) {
|
||||||
CompoundTag root;
|
sendJson(exchange, 400, errorJson(e.getMessage()));
|
||||||
try (InputStream is = new BufferedInputStream(Files.newInputStream(litematicPath))) {
|
return;
|
||||||
root = NbtIo.readCompressed(is, NbtAccounter.unlimitedHeap());
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, 400, errorJson("Failed to read litematic file: " + e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
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));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.placements().isEmpty()) {
|
||||||
|
JsonObject result = okJson();
|
||||||
|
result.addProperty("placed", 0);
|
||||||
|
result.addProperty("format", data.format());
|
||||||
|
sendJson(exchange, 200, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = runOnServerThreadLong(() -> {
|
||||||
|
ServerLevel level = (ServerLevel) bot.level();
|
||||||
|
int placed = 0;
|
||||||
|
for (BlockPlacement bp : data.placements()) {
|
||||||
|
level.setBlock(bp.pos(), bp.state(), 2);
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
JsonObject json = okJson();
|
||||||
|
json.addProperty("placed", placed);
|
||||||
|
json.addProperty("format", data.format());
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
sendJson(exchange, 200, result);
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
sendJson(exchange, 504, errorJson("Server thread timeout (schematic may be too large)"));
|
sendJson(exchange, 504, errorJson("Server thread timeout (schematic may be too large)"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -1255,6 +1250,139 @@ public class ClodHttpServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleGetSchematicInfo(HttpExchange exchange) throws IOException {
|
||||||
|
if (!requirePost(exchange)) return;
|
||||||
|
try {
|
||||||
|
JsonObject body = readJsonBody(exchange);
|
||||||
|
String name = body.get("name").getAsString();
|
||||||
|
int originX = body.get("x").getAsInt();
|
||||||
|
int originY = body.get("y").getAsInt();
|
||||||
|
int originZ = body.get("z").getAsInt();
|
||||||
|
|
||||||
|
if (!name.matches("[a-zA-Z0-9_\\-]+")) {
|
||||||
|
sendJson(exchange, 400, errorJson("Invalid schematic name. Only letters, numbers, underscore, and hyphen allowed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchematicData data;
|
||||||
|
try {
|
||||||
|
data = parseSchematic(name, originX, originY, originZ);
|
||||||
|
} catch (java.io.FileNotFoundException e) {
|
||||||
|
sendJson(exchange, 404, errorJson(e.getMessage()));
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
sendJson(exchange, 400, errorJson(e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group blocks by world Y using TreeMap for natural sort order
|
||||||
|
java.util.TreeMap<Integer, Integer> layerCounts = new java.util.TreeMap<>();
|
||||||
|
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
|
||||||
|
int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE;
|
||||||
|
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (BlockPlacement bp : data.placements()) {
|
||||||
|
int y = bp.pos().getY();
|
||||||
|
layerCounts.merge(y, 1, Integer::sum);
|
||||||
|
minX = Math.min(minX, bp.pos().getX());
|
||||||
|
maxX = Math.max(maxX, bp.pos().getX());
|
||||||
|
minY = Math.min(minY, bp.pos().getY());
|
||||||
|
maxY = Math.max(maxY, bp.pos().getY());
|
||||||
|
minZ = Math.min(minZ, bp.pos().getZ());
|
||||||
|
maxZ = Math.max(maxZ, bp.pos().getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = okJson();
|
||||||
|
result.addProperty("format", data.format());
|
||||||
|
result.addProperty("total_blocks", data.placements().size());
|
||||||
|
|
||||||
|
if (!data.placements().isEmpty()) {
|
||||||
|
JsonObject size = new JsonObject();
|
||||||
|
size.addProperty("x", maxX - minX + 1);
|
||||||
|
size.addProperty("y", maxY - minY + 1);
|
||||||
|
size.addProperty("z", maxZ - minZ + 1);
|
||||||
|
result.add("size", size);
|
||||||
|
} else {
|
||||||
|
JsonObject size = new JsonObject();
|
||||||
|
size.addProperty("x", 0);
|
||||||
|
size.addProperty("y", 0);
|
||||||
|
size.addProperty("z", 0);
|
||||||
|
result.add("size", size);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray layers = new JsonArray();
|
||||||
|
for (var entry : layerCounts.entrySet()) {
|
||||||
|
JsonObject layer = new JsonObject();
|
||||||
|
layer.addProperty("y", entry.getKey());
|
||||||
|
layer.addProperty("block_count", entry.getValue());
|
||||||
|
layers.add(layer);
|
||||||
|
}
|
||||||
|
result.add("layers", layers);
|
||||||
|
|
||||||
|
sendJson(exchange, 200, result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendJson(exchange, 500, errorJson(e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlaceSchematicLayer(HttpExchange exchange) throws IOException {
|
||||||
|
if (!requirePost(exchange)) return;
|
||||||
|
try {
|
||||||
|
ClodEntity bot = requireBot(exchange);
|
||||||
|
if (bot == null) return;
|
||||||
|
|
||||||
|
JsonObject body = readJsonBody(exchange);
|
||||||
|
String name = body.get("name").getAsString();
|
||||||
|
int originX = body.get("x").getAsInt();
|
||||||
|
int originY = body.get("y").getAsInt();
|
||||||
|
int originZ = body.get("z").getAsInt();
|
||||||
|
int layerY = body.get("layer_y").getAsInt();
|
||||||
|
|
||||||
|
if (!name.matches("[a-zA-Z0-9_\\-]+")) {
|
||||||
|
sendJson(exchange, 400, errorJson("Invalid schematic name. Only letters, numbers, underscore, and hyphen allowed."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchematicData data;
|
||||||
|
try {
|
||||||
|
data = parseSchematic(name, originX, originY, originZ);
|
||||||
|
} catch (java.io.FileNotFoundException e) {
|
||||||
|
sendJson(exchange, 404, errorJson(e.getMessage()));
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
sendJson(exchange, 400, errorJson(e.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only blocks at the requested Y layer
|
||||||
|
List<BlockPlacement> layerPlacements = new ArrayList<>();
|
||||||
|
for (BlockPlacement bp : data.placements()) {
|
||||||
|
if (bp.pos().getY() == layerY) {
|
||||||
|
layerPlacements.add(bp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject result = runOnServerThread(() -> {
|
||||||
|
ServerLevel level = (ServerLevel) bot.level();
|
||||||
|
int placed = 0;
|
||||||
|
for (BlockPlacement bp : layerPlacements) {
|
||||||
|
level.setBlock(bp.pos(), bp.state(), 2);
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
JsonObject json = okJson();
|
||||||
|
json.addProperty("placed", placed);
|
||||||
|
json.addProperty("layer_y", layerY);
|
||||||
|
json.addProperty("format", data.format());
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
sendJson(exchange, 200, result);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
sendJson(exchange, 504, errorJson("Server thread timeout"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendJson(exchange, 400, errorJson(e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleListSchematics(HttpExchange exchange) throws IOException {
|
private void handleListSchematics(HttpExchange exchange) throws IOException {
|
||||||
if (!requireGet(exchange)) return;
|
if (!requireGet(exchange)) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user