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