diff --git a/build.gradle b/build.gradle index d62d26e..6709e67 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' -version = '0.6.5' +version = '0.7.0' group = 'com.nilsgawlik.gdmchttp' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = 'gdmchttp' diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java index bdcd78d..bd61361 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java @@ -23,18 +23,24 @@ public BiomesHandler(MinecraftServer mcServer) { protected void internalHandle(HttpExchange httpExchange) throws IOException { // query parameters Map queryParams = parseQueryString(httpExchange.getRequestURI().getRawQuery()); + + // GET: x, y, z positions int x; int y; int z; + + // GET: Ranges in the x, y, z directions (can be negative). Defaults to 1. int dx; int dy; int dz; + String dimension; try { x = Integer.parseInt(queryParams.getOrDefault("x", "0")); y = Integer.parseInt(queryParams.getOrDefault("y", "0")); z = Integer.parseInt(queryParams.getOrDefault("z", "0")); + dx = Integer.parseInt(queryParams.getOrDefault("dx", "1")); dy = Integer.parseInt(queryParams.getOrDefault("dy", "1")); dz = Integer.parseInt(queryParams.getOrDefault("dz", "1")); @@ -45,15 +51,19 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { throw new HandlerBase.HttpException(message, 400); } - Headers reqestHeaders = httpExchange.getRequestHeaders(); - String contentType = getHeader(reqestHeaders, "Accept", "*/*"); - boolean returnJson = contentType.equals("application/json") || contentType.equals("text/json"); + // Check if clients wants a response in a JSON format. If not, return response in plain text. + Headers requestHeaders = httpExchange.getRequestHeaders(); + String acceptHeader = getHeader(requestHeaders, "Accept", "*/*"); + boolean returnJson = hasJsonTypeInHeader(acceptHeader); String method = httpExchange.getRequestMethod().toLowerCase(); + String responseString; if (method.equals("get")) { ServerLevel serverLevel = getServerLevel(dimension); + + // Calculate boundaries of area of blocks to gather biome information on. int xOffset = x + dx; int xMin = Math.min(x, xOffset); int xMax = Math.max(x, xOffset); @@ -65,12 +75,18 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { int zOffset = z + dz; int zMin = Math.min(z, zOffset); int zMax = Math.max(z, zOffset); + if (returnJson) { + // Create a JsonArray with JsonObject, each contain a key-value pair for + // the x, y, z position and the namespaced biome name. JsonArray jsonArray = new JsonArray(); for (int rangeX = xMin; rangeX < xMax; rangeX++) { for (int rangeY = yMin; rangeY < yMax; rangeY++) { for (int rangeZ = zMin; rangeZ < zMax; rangeZ++) { BlockPos blockPos = new BlockPos(rangeX, rangeY, rangeZ); + if (serverLevel.getBiome(blockPos).unwrapKey().isEmpty()) { + continue; + } String biomeName = serverLevel.getBiome(blockPos).unwrapKey().get().location().toString(); JsonObject json = new JsonObject(); json.addProperty("id", biomeName); @@ -83,11 +99,16 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { } responseString = new Gson().toJson(jsonArray); } else { + // Create list of \n-separated strings containing the space-separated + // x, y, z position and the namespaced biome name. ArrayList biomesList = new ArrayList<>(); for (int rangeX = xMin; rangeX < xMax; rangeX++) { for (int rangeY = yMin; rangeY < yMax; rangeY++) { for (int rangeZ = zMin; rangeZ < zMax; rangeZ++) { BlockPos blockPos = new BlockPos(rangeX, rangeY, rangeZ); + if (serverLevel.getBiome(blockPos).unwrapKey().isEmpty()) { + continue; + } String biomeName = serverLevel.getBiome(blockPos).unwrapKey().get().location().toString(); biomesList.add(rangeX + " " + rangeY + " " + rangeZ + " " + biomeName); } @@ -96,16 +117,16 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { responseString = String.join("\n", biomesList); } } else { - throw new HandlerBase.HttpException("Method not allowed. Only GET requests are supported.", 405); + throw new HttpException("Method not allowed. Only GET requests are supported.", 405); } - Headers headers = httpExchange.getResponseHeaders(); - addDefaultHeaders(headers); - - if(returnJson) { - headers.add("Content-Type", "application/json; charset=UTF-8"); + // Response headers + Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); + if (returnJson) { + addResponseHeadersContentTypeJson(responseHeaders); } else { - headers.add("Content-Type", "text/plain; charset=UTF-8"); + addResponseHeadersContentTypePlain(responseHeaders); } resolveRequest(httpExchange, responseString); diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java index 9d17610..40b39f4 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java @@ -12,9 +12,11 @@ import net.minecraft.commands.arguments.coordinates.BlockPosArgument; import net.minecraft.commands.arguments.coordinates.Coordinates; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; import net.minecraft.core.Registry; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Clearable; @@ -32,7 +34,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -56,16 +57,31 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { // query parameters Map queryParams = parseQueryString(httpExchange.getRequestURI().getRawQuery()); + + // PUT/GET: x, y, z positions int x; int y; int z; + + // GET: Ranges in the x, y, z directions (can be negative). Defaults to 1. int dx; int dy; int dz; - boolean includeData; + + // GET: Whether to include block state https://minecraft.fandom.com/wiki/Block_states boolean includeState; + + // GET: Whether to include block entity data https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format + boolean includeData; + + // PUT: Defaults to true. If true, update neighbouring blocks after placement. boolean doBlockUpdates; + + // PUT: Defaults to false. If true, block updates cause item drops after placement. boolean spawnDrops; + + // PUT: Overrides both doBlockUpdates and spawnDrops if set. For more information see #getBlockFlags and + // https://minecraft.fandom.com/wiki/Block_update int customFlags; // -1 == no custom flags try { @@ -77,89 +93,136 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { dy = Integer.parseInt(queryParams.getOrDefault("dy", "1")); dz = Integer.parseInt(queryParams.getOrDefault("dz", "1")); - includeData = Boolean.parseBoolean(queryParams.getOrDefault("includeData", "false")); includeState = Boolean.parseBoolean(queryParams.getOrDefault("includeState", "false")); + includeData = Boolean.parseBoolean(queryParams.getOrDefault("includeData", "false")); + doBlockUpdates = Boolean.parseBoolean(queryParams.getOrDefault("doBlockUpdates", "true")); + spawnDrops = Boolean.parseBoolean(queryParams.getOrDefault("spawnDrops", "false")); + customFlags = Integer.parseInt(queryParams.getOrDefault("customFlags", "-1"), 2); dimension = queryParams.getOrDefault("dimension", null); } catch (NumberFormatException e) { String message = "Could not parse query parameter: " + e.getMessage(); - throw new HandlerBase.HttpException(message, 400); + throw new HttpException(message, 400); } - // if content type is application/json use that otherwise return text - Headers reqestHeaders = httpExchange.getRequestHeaders(); - String contentType = getHeader(reqestHeaders, "Accept", "*/*"); - boolean returnJson = contentType.equals("application/json") || contentType.equals("text/json"); + // Check if clients wants a response in a JSON format. If not, return response in plain text. + Headers requestHeaders = httpExchange.getRequestHeaders(); + String acceptHeader = getHeader(requestHeaders, "Accept", "*/*"); + boolean returnJson = hasJsonTypeInHeader(acceptHeader); - // construct response String method = httpExchange.getRequestMethod().toLowerCase(); + String responseString; if (method.equals("put")) { - InputStream bodyStream = httpExchange.getRequestBody(); - List body = new BufferedReader(new InputStreamReader(bodyStream)) - .lines().toList(); + String contentTypeHeader = getHeader(requestHeaders,"Content-Type", "*/*"); + boolean parseRequestAsJson = hasJsonTypeInHeader(contentTypeHeader); - List returnValues = new LinkedList<>(); - - int blockFlags = customFlags >= 0? customFlags : getBlockFlags(doBlockUpdates, spawnDrops); + int blockFlags = customFlags >= 0 ? customFlags : getBlockFlags(doBlockUpdates, spawnDrops); + // Create instance of CommandSourceStack to use as a point of origin for any relative positioned blocks. CommandSourceStack commandSourceStack = cmdSrc.withPosition(new Vec3(x, y, z)); - for(String line : body) { - String returnValue; - try { - StringReader sr = new StringReader(line); - Coordinates li = null; + ArrayList returnValues = new ArrayList<>(); + + InputStream bodyStream = httpExchange.getRequestBody(); + + if (parseRequestAsJson) { + JsonArray blockPlacementList = JsonParser.parseReader(new InputStreamReader(bodyStream)).getAsJsonArray(); + for (JsonElement blockPlacement : blockPlacementList) { + String returnValue; + JsonObject blockPlacementItem = blockPlacement.getAsJsonObject(); try { - li = BlockPosArgument.blockPos().parse(sr); - sr.skip(); + + // Parse block position x y z. Use the position of the command source (set with the URL query parameters) if not defined in + // the block placement item JsonObject. Valid values may be any positive or negative integer and can use tilde or caret notation + // (see: https://minecraft.fandom.com/wiki/Coordinates#Relative_world_coordinates). + String posXString = blockPlacementItem.has("x") ? blockPlacementItem.get("x").getAsString() : String.valueOf(x); + String posYString = blockPlacementItem.has("y") ? blockPlacementItem.get("y").getAsString() : String.valueOf(y); + String posZString = blockPlacementItem.has("z") ? blockPlacementItem.get("z").getAsString() : String.valueOf(z); + BlockPos blockPos = getBlockPosFromString( + "%s %s %s".formatted(posXString, posYString, posZString), + commandSourceStack + ); + + // Skip if block id is missing + if (!blockPlacementItem.has("id")) { + returnValues.add("block id is missing in " + blockPlacement); + continue; + } + String blockId = blockPlacementItem.get("id").getAsString(); + + // Check if JSON contains an JsonObject or string for block state. Use an empty block state string ("[]") if nothing suitable is found. + String blockStateString = "[]"; + if (blockPlacementItem.has("state")) { + if (blockPlacementItem.get("state").isJsonObject()) { + blockStateString = getBlockStateStringFromJSONObject(blockPlacementItem.get("state").getAsJsonObject()); + } else if (blockPlacementItem.get("state").isJsonPrimitive()) { + blockStateString = blockPlacementItem.get("state").getAsString(); + } + } + + // Pass block Id and block state string into a Stringreader with the the block state parser. + HolderLookup blockStateArgumetBlocks = new CommandBuildContext(commandSourceStack.registryAccess()).holderLookup(Registry.BLOCK_REGISTRY); + BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock(blockStateArgumetBlocks, new StringReader( + blockId + blockStateString + ), true); + BlockState blockState = parsedBlockState.blockState(); + + // If data field is present in JsonObject serialize to to a string so it can be parsed to a CompoundTag to set as NBT block entity data + // for this block placement. + CompoundTag compoundTag = null; + if (blockPlacementItem.has("data")) { + compoundTag = TagParser.parseTag(blockPlacementItem.get("data").toString()); + } + + // Attempt to place block in the world. + returnValue = setBlock(blockPos, blockState, compoundTag, blockFlags) + ""; + } catch (CommandSyntaxException e) { - sr = new StringReader(line); // TODO maybe delete this + returnValue = e.getMessage(); } - int xx, yy, zz; - if (li != null) { - xx = (int)Math.round(li.getPosition(commandSourceStack).x); - yy = (int)Math.round(li.getPosition(commandSourceStack).y); - zz = (int)Math.round(li.getPosition(commandSourceStack).z); - } else { - xx = x; - yy = y; - zz = z; - } - BlockPos blockPos = new BlockPos(xx, yy, zz); + returnValues.add(returnValue); + } + + } else { + List body = new BufferedReader(new InputStreamReader(bodyStream)) + .lines().toList(); - HolderLookup blockStateArgumetBlocks = new CommandBuildContext(commandSourceStack.registryAccess()).holderLookup(Registry.BLOCK_REGISTRY); - BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock(blockStateArgumetBlocks, sr, true); - BlockState blockState = parsedBlockState.blockState(); - CompoundTag compoundTag = parsedBlockState.nbt(); + for (String line : body) { + String returnValue; + try { + StringReader sr = new StringReader(line); + BlockPos blockPos = getBlockPosFromString(sr, commandSourceStack); - returnValue = setBlock(blockPos, blockState, compoundTag, blockFlags) + ""; + HolderLookup blockStateArgumetBlocks = new CommandBuildContext(commandSourceStack.registryAccess()).holderLookup(Registry.BLOCK_REGISTRY); + BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock(blockStateArgumetBlocks, sr, true); + BlockState blockState = parsedBlockState.blockState(); + CompoundTag compoundTag = parsedBlockState.nbt(); - } catch (CommandSyntaxException e) { - returnValue = e.getMessage(); - } - returnValues.add(returnValue); - } - if (returnJson) { - JsonObject json = new JsonObject(); - JsonArray resultsArray = new JsonArray(); + returnValue = setBlock(blockPos, blockState, compoundTag, blockFlags) + ""; - for(String s : returnValues) { - resultsArray.add(s); + } catch (CommandSyntaxException e) { + returnValue = e.getMessage(); + } + returnValues.add(returnValue); } + } - json.add("results", resultsArray); - responseString = new Gson().toJson(json); + // Set response as a list of "1" (block was placed), "0" (block was not placed) or an exception string if something went wrong placing the block. + if (returnJson) { + responseString = new Gson().toJson(returnValues); } else { responseString = String.join("\n", returnValues); } } else if (method.equals("get")) { + + // Calculate boundaries of area of blocks to gather information on. int xOffset = x + dx; int xMin = Math.min(x, xOffset); int xMax = Math.max(x, xOffset); @@ -173,6 +236,9 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { int zMax = Math.max(z, zOffset); if (returnJson) { + // Create a JsonArray with JsonObject, each contain a key-value pair for + // the x, y, z position, the block ID, the block state (if requested and available) + // and the block entity data (if requested and available). JsonArray jsonArray = new JsonArray(); for (int rangeX = xMin; rangeX < xMax; rangeX++) { for (int rangeY = yMin; rangeY < yMax; rangeY++) { @@ -196,6 +262,9 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { } responseString = new Gson().toJson(jsonArray); } else { + // Create list of \n-separated strings containing the x, y, z position space-separated, + // the block ID, the block state (if requested and available) between square brackets + // and the block entity data (if requested and available) between curly brackets. ArrayList responseList = new ArrayList<>(); for (int rangeX = xMin; rangeX < xMax; rangeX++) { for (int rangeY = yMin; rangeY < yMax; rangeY++) { @@ -215,22 +284,75 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { responseString = String.join("\n", responseList); } } else { - throw new HandlerBase.HttpException("Method not allowed. Only PUT and GET requests are supported.", 405); + throw new HttpException("Method not allowed. Only PUT and GET requests are supported.", 405); } - //headers - Headers headers = httpExchange.getResponseHeaders(); - addDefaultHeaders(headers); - - if(returnJson) { - headers.add("Content-Type", "application/json; charset=UTF-8"); + // Response headers + Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); + if (returnJson) { + addResponseHeadersContentTypeJson(responseHeaders); } else { - headers.add("Content-Type", "text/plain; charset=UTF-8"); + addResponseHeadersContentTypePlain(responseHeaders); } resolveRequest(httpExchange, responseString); } + private BlockState getBlockStateAtPosition(BlockPos pos) { + ServerLevel serverLevel = getServerLevel(dimension); + return serverLevel.getBlockState(pos); + } + + /** + * Parse block position x y z. + * Valid values may be any positive or negative integer and can use tilde or caret notation. + * see: Relative World Coordinates - Minecraft Wiki + * + * @param s {@code String} which may or may not contain a valid block position coordinate. + * @param commandSourceStack Origin for relative coordinates. + * @return Valid {@link BlockPos}. + * @throws CommandSyntaxException If input string cannot be parsed into a valid {@link BlockPos}. + */ + private static BlockPos getBlockPosFromString(String s, CommandSourceStack commandSourceStack) throws CommandSyntaxException { + return getBlockPosFromString(new StringReader(s), commandSourceStack); + } + + /** + * Parse block position x y z. + * Valid values may be any positive or negative integer and can use tilde or caret notation. + * see: Relative World Coordinates - Minecraft Wiki + * + * @param blockPosStringReader {@code StringReader} which may or may not contain a valid block position coordinate. + * @param commandSourceStack Origin for relative coordinates. + * @return Valid {@link BlockPos}. + * @throws CommandSyntaxException If input string reader cannot be parsed into a valid {@link BlockPos}. + */ + private static BlockPos getBlockPosFromString(StringReader blockPosStringReader, CommandSourceStack commandSourceStack) throws CommandSyntaxException { + Coordinates coordinates = BlockPosArgument.blockPos().parse(blockPosStringReader); + blockPosStringReader.skip(); + return coordinates.getBlockPos(commandSourceStack); + } + + /** + * @param json Valid flat {@link JsonObject} of keys with primitive values (Strings, numbers, booleans) + * @return {@code String} which can be parsed by {@link BlockStateParser} and should be the same as the return value of {@link BlockState#toString()} of the {@link BlockState} resulting from that parser. + */ + private static String getBlockStateStringFromJSONObject(JsonObject json) { + ArrayList blockStateList = new ArrayList<>(); + for (Map.Entry element : json.entrySet()) { + blockStateList.add(element.getKey() + "=" + element.getValue()); + } + return '[' + String.join(",", blockStateList) + ']'; + } + + /** + * @param pos Position in the world the block should be placed. + * @param blockState Contains both the state as well as the material of the block. + * @param blockEntityData Optional tag of NBT data to be associated with the block (eg. contents of a chest). + * @param flags Block placement flags (see {@link #getBlockFlags(boolean, boolean)} and {@link Block} for more information). + * @return return 1 if block has been placed or 0 if it couldn't be placed at the given location. + */ private int setBlock(BlockPos pos, BlockState blockState, CompoundTag blockEntityData, int flags) { ServerLevel serverLevel = getServerLevel(dimension); @@ -244,40 +366,58 @@ private int setBlock(BlockPos pos, BlockState blockState, CompoundTag blockEntit existingBlockEntity.deserializeNBT(blockEntityData); } } + + // If block placement flags allow for updating neighbouring blocks, update the shape neighbouring blocks + // in the north, west, south, east, up, down directions. + if ((flags & Block.UPDATE_NEIGHBORS) != 0 && (flags & Block.UPDATE_KNOWN_SHAPE) == 0) { + for (Direction direction : Direction.values()) { + BlockPos neighbourPosition = pos.relative(direction); + BlockState neighbourBlockState = serverLevel.getBlockState(neighbourPosition); + neighbourBlockState.updateNeighbourShapes(serverLevel, neighbourPosition, flags); + } + } + return 1; } else { return 0; } } + /** + * @param pos Position of block in the world. + * @return Namespaced name of the block material. + */ private String getBlockAsStr(BlockPos pos) { - ServerLevel serverLevel = getServerLevel(dimension); - - BlockState bs = serverLevel.getBlockState(pos); + BlockState bs = getBlockStateAtPosition(pos); return Objects.requireNonNull(getBlockRegistryName(bs)); } + /** + * @param pos Position of block in the world. + * @return {@link JsonObject} containing the block state data of the block at the given position. + */ private JsonObject getBlockStateAsJsonObject(BlockPos pos) { - ServerLevel serverLevel = getServerLevel(dimension); - - BlockState bs = serverLevel.getBlockState(pos); - + BlockState bs = getBlockStateAtPosition(pos); JsonObject stateJsonObject = new JsonObject(); - bs.getValues().entrySet().stream().map(propertyToStringPairFunction).filter(Objects::nonNull).forEach(pair -> stateJsonObject.add(pair.getKey(), new JsonPrimitive(pair.getValue()))); return stateJsonObject; } + /** + * @param pos Position of block in the world. + * @return {@link String} containing the block state data of the block at the given position. + */ private String getBlockStateAsStr(BlockPos pos) { - ServerLevel serverLevel = getServerLevel(dimension); - - BlockState bs = serverLevel.getBlockState(pos); - + BlockState bs = getBlockStateAtPosition(pos); return '[' + bs.getValues().entrySet().stream().map(propertyToStringFunction).collect(Collectors.joining(",")) + - ']'; + ']'; } + /** + * @param pos Position of block in the world. + * @return {@link JsonObject} containing the block entity data of the block at the given position. + */ private JsonObject getBlockDataAsJsonObject(BlockPos pos) { ServerLevel serverLevel = getServerLevel(dimension); JsonObject dataJsonObject = new JsonObject(); @@ -293,6 +433,10 @@ private JsonObject getBlockDataAsJsonObject(BlockPos pos) { return dataJsonObject; } + /** + * @param pos Position of block in the world. + * @return {@link String} containing the block entity data of the block at the given position. + */ private String getBlockDataAsStr(BlockPos pos) { ServerLevel serverLevel = getServerLevel(dimension); String str = "{}"; @@ -304,9 +448,18 @@ private String getBlockDataAsStr(BlockPos pos) { return str; } + /** + * @param blockState Instance of {@link BlockState} to extract {@link Block} from. + * @return Namespaced name of the block material. + */ public static String getBlockRegistryName(BlockState blockState) { return getBlockRegistryName(blockState.getBlock()); } + + /** + * @param block Instance of {@link Block} to find in {@link ForgeRegistries#BLOCKS}. + * @return Namespaced name of the block material. + */ public static String getBlockRegistryName(Block block) { return Objects.requireNonNull(ForgeRegistries.BLOCKS.getKey(block)).toString(); } @@ -323,7 +476,7 @@ public static int getBlockFlags(boolean doBlockUpdates, boolean spawnDrops) { * 64 will signify the block is being moved. */ // construct flags - return 2 | ( doBlockUpdates? 1 : (32 | 16) ) | ( spawnDrops? 0 : 32 ); + return Block.UPDATE_CLIENTS | (doBlockUpdates ? Block.UPDATE_NEIGHBORS : (Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_KNOWN_SHAPE)) | (spawnDrops ? 0 : Block.UPDATE_SUPPRESS_DROPS); } // function that converts a bunch of Property/Comparable pairs into strings that look like 'property=value' diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java index ce8b88c..9b1bc82 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java @@ -59,18 +59,19 @@ public BuildAreaHandler(MinecraftServer mcServer) { public void internalHandle(HttpExchange httpExchange) throws IOException { // throw errors when appropriate String method = httpExchange.getRequestMethod().toLowerCase(); - if(!method.equals("get")) { - throw new HandlerBase.HttpException("Please use GET method to request the build area.", 405); + if (!method.equals("get")) { + throw new HttpException("Please use GET method to request the build area.", 405); } - if(buildArea == null) { - throw new HandlerBase.HttpException("No build area is specified. Use the buildarea command inside Minecraft to set a build area.",404); + if (buildArea == null) { + throw new HttpException("No build area is specified. Use the buildarea command inside Minecraft to set a build area.", 404); } String responseString = new Gson().toJson(buildArea); - Headers headers = httpExchange.getResponseHeaders(); - headers.add("Content-Type", "application/json; charset=UTF-8"); + Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); + addResponseHeadersContentTypeJson(responseHeaders); resolveRequest(httpExchange, responseString); } diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java index 60d51af..b155e62 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java @@ -6,6 +6,7 @@ import com.sun.net.httpserver.HttpExchange; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.LevelChunk; @@ -19,7 +20,6 @@ public class ChunkHandler extends HandlerBase { - String dimension; public ChunkHandler(MinecraftServer mcServer) { super(mcServer); } @@ -30,36 +30,49 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { // query parameters Map queryParams = parseQueryString(httpExchange.getRequestURI().getRawQuery()); + // GET: Chunk coordinate at origin. int chunkX; int chunkZ; + + // GET: Ranges in the x and z directions (can be negative). Defaults to 1. int chunkDX; int chunkDZ; + + String dimension; + try { chunkX = Integer.parseInt(queryParams.getOrDefault("x", "0")); chunkZ = Integer.parseInt(queryParams.getOrDefault("z", "0")); + chunkDX = Integer.parseInt(queryParams.getOrDefault("dx", "1")); chunkDZ = Integer.parseInt(queryParams.getOrDefault("dz", "1")); + dimension = queryParams.getOrDefault("dimension", null); } catch (NumberFormatException e) { String message = "Could not parse query parameter: " + e.getMessage(); - throw new HandlerBase.HttpException(message, 400); + throw new HttpException(message, 400); } String method = httpExchange.getRequestMethod().toLowerCase(); if(!method.equals("get")) { - throw new HandlerBase.HttpException("Method not allowed. Only GET requests are supported.", 405); + throw new HttpException("Method not allowed. Only GET requests are supported.", 405); } - // with this header we return pure NBT binary - // if content type is application/json use that otherwise return text + // Check if clients wants a response in plain-text or JSON format. If not, return response + // in a binary format. Headers requestHeaders = httpExchange.getRequestHeaders(); - String contentType = getHeader(requestHeaders, "Accept", "*/*"); - boolean returnBinary = contentType.equals("application/octet-stream"); - boolean returnJson = contentType.equals("application/json") || contentType.equals("text/json"); + String acceptHeader = getHeader(requestHeaders, "Accept", "*/*"); + boolean returnPlainText = acceptHeader.equals("text/plain"); + boolean returnJson = hasJsonTypeInHeader(acceptHeader); + + // If "Accept-Encoding" header is set to "gzip" and the client expects a binary format, + // compress the result using GZIP before sending out the response. + String acceptEncodingHeader = getHeader(requestHeaders, "Accept-Encoding", "*"); + boolean returnCompressed = acceptEncodingHeader.equals("gzip"); - // construct response ServerLevel serverLevel = getServerLevel(dimension); + // Gather all chunk data within the given range. CompletableFuture cfs = CompletableFuture.supplyAsync(() -> { int xOffset = chunkX + chunkDX; int xMin = Math.min(chunkX, xOffset); @@ -69,9 +82,9 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { int zMin = Math.min(chunkZ, zOffset); int zMax = Math.max(chunkZ, zOffset); ListTag returnList = new ListTag(); - for(int z = zMin; z < zMax; z++) - for(int x = xMin; x < xMax; x++) { - LevelChunk chunk = serverLevel.getChunk(x, z); + for (int rangeZ = zMin; rangeZ < zMax; rangeZ++) + for (int rangeX = xMin; rangeX < xMax; rangeX++) { + LevelChunk chunk = serverLevel.getChunk(rangeX, rangeZ); CompoundTag chunkNBT = ChunkSerializer.write(serverLevel, chunk); returnList.add(chunkNBT); } @@ -89,29 +102,34 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { bodyNBT.putInt("ChunkDZ", chunkDZ); // Response header and response body - Headers headers = httpExchange.getResponseHeaders(); - if (returnBinary) { - headers.add("Content-Type", "application/octet-stream"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(baos); + Headers responseHeaders = httpExchange.getResponseHeaders(); + if (returnPlainText) { + addResponseHeadersContentTypePlain(responseHeaders); - CompoundTag containterNBT = new CompoundTag(); - containterNBT.put("file", bodyNBT); - containterNBT.write(dos); - dos.flush(); - byte[] responseBytes = baos.toByteArray(); + String responseString = bodyNBT.toString(); - resolveRequest(httpExchange, responseBytes); + resolveRequest(httpExchange, responseString); } else if (returnJson) { - headers.add("Content-Type", "application/json; charset=UTF-8"); + addResponseHeadersContentTypeJson(responseHeaders); + String responseString = JsonParser.parseString((new JsonTagVisitor()).visit(bodyNBT)).toString(); resolveRequest(httpExchange, responseString); } else { - headers.add("Content-Type", "text/plain; charset=UTF-8"); - String responseString = bodyNBT.toString(); + addResponseHeadersContentTypeBinary(responseHeaders); - resolveRequest(httpExchange, responseString); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + if (returnCompressed) { + responseHeaders.add("Content-Encoding", "gzip"); + NbtIo.writeCompressed(bodyNBT, dos); + } else { + NbtIo.write(bodyNBT, dos); + } + dos.flush(); + byte[] responseBytes = baos.toByteArray(); + + resolveRequest(httpExchange, responseBytes); } } } \ No newline at end of file diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java index 4800194..fcfe1bc 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java @@ -13,13 +13,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; public class CommandHandler extends HandlerBase { private static final Logger LOGGER = LogManager.getLogger(); - private String dimension; - public CommandHandler(MinecraftServer mcServer) { super(mcServer); } @@ -28,12 +25,13 @@ public CommandHandler(MinecraftServer mcServer) { public void internalHandle(HttpExchange httpExchange) throws IOException { Map queryParams = parseQueryString(httpExchange.getRequestURI().getRawQuery()); - dimension = queryParams.getOrDefault("dimension", null); + + String dimension = queryParams.getOrDefault("dimension", null); // execute command(s) InputStream bodyStream = httpExchange.getRequestBody(); List commands = new BufferedReader(new InputStreamReader(bodyStream)) - .lines().filter(a -> a.length() > 0).collect(Collectors.toList()); + .lines().filter(a -> a.length() > 0).toList(); CommandSourceStack cmdSrc = createCommandSource("GDMC-CommandHandler", mcServer, dimension); @@ -56,9 +54,10 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { outputs.add(result); } - //headers - Headers headers = httpExchange.getResponseHeaders(); - headers.add("Content-Type", "text/plain; charset=UTF-8"); + // Response headers + Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); + addResponseHeadersContentTypePlain(responseHeaders); // body String responseString = String.join("\n", outputs); diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java index 565a987..1e2efbe 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java @@ -15,6 +15,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; import java.io.*; import java.net.URLDecoder; @@ -32,7 +33,7 @@ public HttpException(String message, int statusCode) { } } - private static final Logger LOGGER = LogManager.getLogger(); + protected static final Logger LOGGER = LogManager.getLogger(); MinecraftServer mcServer; public HandlerBase(MinecraftServer mcServer) { @@ -54,12 +55,12 @@ public void handle(HttpExchange httpExchange) throws IOException { outputStream.write(responseBytes); outputStream.close(); -// LOGGER.log(Level.ERROR, e.message); + LOGGER.log(Level.ERROR, e.message); } catch (Exception e) { // create a response string with stacktrace String stackTrace = ExceptionUtils.getStackTrace(e); - String responseString = String.format("Internal server error: %s\n%s", e.toString(), stackTrace); + String responseString = String.format("Internal server error: %s\n%s", e, stackTrace); byte[] responseBytes = responseString.getBytes(StandardCharsets.UTF_8); Headers headers = httpExchange.getResponseHeaders(); headers.set("Content-Type", "text/plain; charset=UTF-8"); @@ -74,10 +75,20 @@ public void handle(HttpExchange httpExchange) throws IOException { } } - public ServerLevel getServerLevel() { - return getServerLevel(null); - } - + /** + * Get specific level (sometimes called a "dimension") of a Minecraft world. + * In a conventional Minecraft world the following levels are expected to be + * present: {@code "overworld"}, {@code "the_nether"} and {@code "the_end"}. + * A world may be modified to have a different list of levels. + * + * @param levelName name of level as it's path name appears in world's + * list of levels. For convenience "the_nether" and + * "the_end" can be shorted to "nether" and "end" + * respectively but still return the same level. + * If no level with the given name is found or if the + * given name is {@code null}, return the overworld. + * @return A level on {@link #mcServer} + */ public ServerLevel getServerLevel(String levelName) { if (levelName != null) { levelName = levelName.toLowerCase(); @@ -93,13 +104,68 @@ public ServerLevel getServerLevel(String levelName) { return mcServer.overworld(); } + /** + * Method that an endpoint handler class can use for executing a function. + * + * @param httpExchange HTTP request exhanger + * @throws IOException Any errors caught should be dealt with in {@link #handle(HttpExchange)}. + */ protected abstract void internalHandle(HttpExchange httpExchange) throws IOException; - protected static void addDefaultHeaders(Headers headers) { + protected static String getHeader(Headers headers, String key, String defaultValue) { + List list = headers.get(key); + if(list == null || list.size() == 0) + return defaultValue; + else + return list.get(0); + } + + /** + * @param header single header as string from request or response headers + * @return {@code true} if header string has a common description of a JSON Content-Type. + */ + protected static boolean hasJsonTypeInHeader(String header) { + return header.equals("application/json") || header.equals("text/json"); + } + + /** + * Helper to add basic headers to headers of a response. + * + * @param headers request or response headers + */ + protected static void addDefaultResponseHeaders(Headers headers) { headers.add("Access-Control-Allow-Origin", "*"); headers.add("Content-Disposition", "inline"); } + /** + * Helper to tell clients that response is formatted as JSON. + * + * @param headers request or response headers + */ + protected static void addResponseHeadersContentTypeJson(Headers headers) { + headers.add("Content-Type", "application/json; charset=UTF-8"); + } + + /** + * Helper to tell clients that response is formatted as plain text. + * + * @param headers request or response headers + */ + protected static void addResponseHeadersContentTypePlain(Headers headers) { + headers.add("Content-Type", "text/plain; charset=UTF-8"); + } + + /** + * Helper to tell clients that response is in a binary format that should be treated as an attachment. + * + * @param headers request or response headers + */ + protected static void addResponseHeadersContentTypeBinary(Headers headers) { + headers.add("Content-Type", "application/octet-stream"); + headers.add("Content-Disposition", "attachment"); + } + protected static void resolveRequest(HttpExchange httpExchange, String responseString) throws IOException { byte[] responseBytes = responseString.getBytes(StandardCharsets.UTF_8); resolveRequest(httpExchange, responseBytes); @@ -112,14 +178,12 @@ protected static void resolveRequest(HttpExchange httpExchange, byte[] responseB outputStream.close(); } - protected static String getHeader(Headers headers, String key, String defaultValue) { - List list = headers.get(key); - if(list == null || list.size() == 0) - return defaultValue; - else - return list.get(0); - } - + /** + * Parse URL query string from {@code httpExchange.getRequestURI().getRawQuery()} into a convenient Map. + * + * @param qs Any string, preferably the query section from an URL. + * @return Map of String-String pairs. + */ protected static Map parseQueryString(String qs) { Map result = new HashMap<>(); if (qs == null) @@ -133,24 +197,30 @@ protected static Map parseQueryString(String qs) { if (next > last) { int eqPos = qs.indexOf('=', last); - try { - if (eqPos < 0 || eqPos > next) - result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), ""); - else - result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"), URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java - } + if (eqPos < 0 || eqPos > next) + result.put(URLDecoder.decode(qs.substring(last, next), StandardCharsets.UTF_8), ""); + else + result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8), URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8)); } last = next + 1; } return result; } + /** + * Helper to create a {@code CommandSourceStack}, which serves as the source to dispatch + * commands from (See {@link CommandHandler}) or as a point of origin to place blocks + * relative from (See {@link BlocksHandler}). + * + * @param name Some unique identifier. + * @param mcServer The Minecraft server in which the {@code CommandSourceStack} is going to be placed. + * @param dimension The dimension (also known as level) on the world of {@code mcServer} in which the {@code CommandSourceStack} is going to be placed. + * @return An instance of {@code CommandSourceStack}. + */ protected CommandSourceStack createCommandSource(String name, MinecraftServer mcServer, String dimension) { CommandSource commandSource = new CommandSource() { @Override - public void sendSystemMessage(Component p_230797_) { + public void sendSystemMessage(@NotNull Component p_230797_) { } diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/MinecraftVersionHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/MinecraftVersionHandler.java index ba44467..990573f 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/MinecraftVersionHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/MinecraftVersionHandler.java @@ -1,5 +1,6 @@ package com.gdmc.httpinterfacemod.handlers; +import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import net.minecraft.server.MinecraftServer; @@ -12,6 +13,16 @@ public MinecraftVersionHandler(MinecraftServer mcServer) { @Override protected void internalHandle(HttpExchange httpExchange) throws IOException { + String method = httpExchange.getRequestMethod().toLowerCase(); + + if (!method.equals("get")) { + throw new HttpException("Method not allowed. Only GET requests are supported.", 405); + } + + Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); + addResponseHeadersContentTypePlain(responseHeaders); + String responseString = mcServer.getServerVersion(); resolveRequest(httpExchange, responseString); } diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java index 9fa6f75..1f8ee6d 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java @@ -20,34 +20,47 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.io.InputStream; import java.util.Map; public class StructureHandler extends HandlerBase { + public StructureHandler(MinecraftServer mcServer) { super(mcServer); } @Override protected void internalHandle(HttpExchange httpExchange) throws IOException { + // query parameters Map queryParams = parseQueryString(httpExchange.getRequestURI().getRawQuery()); + + // POST/GET: x, y, z positions int x; int y; int z; + // GET: Ranges in the x, y, z directions (can be negative). Defaults to 1. int dx; int dy; int dz; + // POST: If set, mirror the input structure on the x or z axis. Valid values are "x", "z" and unset. String mirror; - int rotation; + + // POST: If set, rotate the input structure 0, 1, 2, 3 times in 90° clock-wise. + int rotate; + + // POST: set pivot point for the rotation. Values are relative to origin of the structure. int pivotX; int pivotY; int pivotZ; + + // POST/GET: Whether to include entities (mobs, villagers, items) in placing/getting a structure. boolean includeEntities; String dimension; @@ -62,7 +75,9 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { dz = Integer.parseInt(queryParams.getOrDefault("dz", "1")); mirror = queryParams.getOrDefault("mirror", ""); - rotation = Integer.parseInt(queryParams.getOrDefault("rotate", "0")) % 4; + + rotate = Integer.parseInt(queryParams.getOrDefault("rotate", "0")) % 4; + pivotX = Integer.parseInt(queryParams.getOrDefault("pivotx", "0")); pivotY = Integer.parseInt(queryParams.getOrDefault("pivoty", "0")); pivotZ = Integer.parseInt(queryParams.getOrDefault("pivotz", "0")); @@ -74,35 +89,59 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { throw new HandlerBase.HttpException(message, 400); } - // with this header we return pure NBT binary - // if content type is application/json use that otherwise return text - Headers reqestHeaders = httpExchange.getRequestHeaders(); - String contentType = getHeader(reqestHeaders, "Accept", "*/*"); - boolean returnPlainText = contentType.equals("text/plain"); - boolean returnJson = contentType.equals("application/json") || contentType.equals("text/json"); + // Check if clients wants a response in plain-text or JSON format. If not, return response + // in a binary format. + Headers requestHeaders = httpExchange.getRequestHeaders(); + String acceptHeader = getHeader(requestHeaders, "Accept", "*/*"); + boolean returnPlainText = acceptHeader.equals("text/plain"); + boolean returnJson = hasJsonTypeInHeader(acceptHeader); + + // If "Accept-Encoding" header is set to "gzip" and the client expects a binary format, + // (both default) compress the result using GZIP before sending out the response. + String acceptEncodingHeader = getHeader(requestHeaders, "Accept-Encoding", "gzip"); + boolean returnCompressed = acceptEncodingHeader.equals("gzip"); String method = httpExchange.getRequestMethod().toLowerCase(); + String responseString; if (method.equals("post")) { + // Check if there is a header present stating that the request body is compressed with GZIP. + // Any structure file generated by Minecraft itself using the Structure Block + // (https://minecraft.fandom.com/wiki/Structure_Block) as well as the built-in Structures are + // stored in this compressed format. + String contentEncodingHeader = getHeader(requestHeaders, "Content-Encoding", "*"); + boolean inputShouldBeCompressed = contentEncodingHeader.equals("gzip"); + CompoundTag structureCompound; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + httpExchange.getRequestBody().transferTo(outputStream); try { // Read request body into NBT data compound that can be placed in the world. - InputStream bodyStream = httpExchange.getRequestBody(); - structureCompound = NbtIo.readCompressed(bodyStream); -// TODO detect if file is not GZIPped and parse it some different way. + structureCompound = NbtIo.readCompressed(new ByteArrayInputStream(outputStream.toByteArray())); } catch (Exception exception) { - String message = "Could not process request body: " + exception.getMessage(); - throw new HandlerBase.HttpException(message, 400); + // If header states the content should be compressed but it isn't, throw an error. Otherwise, try + // reading the content again, assuming it is not compressed. + if (inputShouldBeCompressed) { + String message = "Could not process request body: " + exception.getMessage(); + throw new HttpException(message, 400); + } + try { + DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + structureCompound = NbtIo.read(dataInputStream); + } catch (Exception exception1) { + String message = "Could not process request body: " + exception1.getMessage(); + throw new HttpException(message, 400); + } } - // Prepare transformations to the structure + // Prepare transformation settings for the structure. StructurePlaceSettings structurePlaceSettings = new StructurePlaceSettings(); switch (mirror) { case "x" -> structurePlaceSettings.setMirror(Mirror.FRONT_BACK); case "z" -> structurePlaceSettings.setMirror(Mirror.LEFT_RIGHT); } - switch (rotation) { + switch (rotate) { case 1 -> structurePlaceSettings.setRotation(Rotation.CLOCKWISE_90); case 2 -> structurePlaceSettings.setRotation(Rotation.CLOCKWISE_180); case 3 -> structurePlaceSettings.setRotation(Rotation.COUNTERCLOCKWISE_90); @@ -128,6 +167,8 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { BlocksHandler.getBlockFlags(true, false) ); if (hasPlaced) { + // After placement, go through all blocks listed in the structureCompound and place the corresponding block entity data + // stored at key "nbt" using the same placement settings as the structure itself. ListTag blockList = structureCompound.getList("blocks", Tag.TAG_COMPOUND); for (int i = 0; i < blockList.size(); i++) { CompoundTag tag = blockList.getCompound(i); @@ -147,14 +188,22 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { } else { responseString = "0"; } - resolveRequest(httpExchange, responseString); } catch (Exception exception) { String message = "Could place structure: " + exception.getMessage(); - throw new HandlerBase.HttpException(message, 500); + throw new HttpException(message, 400); } + + Headers responseHeaders = httpExchange.getResponseHeaders(); + if (returnJson) { + responseString = "[\"" + responseString + "\"]"; + addResponseHeadersContentTypeJson(responseHeaders); + } else { + addResponseHeadersContentTypePlain(responseHeaders); + } + resolveRequest(httpExchange, responseString); } else if (method.equals("get")) { - ServerLevel serverLevel = getServerLevel(dimension); + // Calculate boundaries of area of blocks to gather information on. int xOffset = x + dx; int xMin = Math.min(x, xOffset); @@ -164,7 +213,9 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { int zOffset = z + dz; int zMin = Math.min(z, zOffset); + // Create StructureTemplate using blocks within the given area of the world. StructureTemplate structureTemplate = new StructureTemplate(); + ServerLevel serverLevel = getServerLevel(dimension); BlockPos origin = new BlockPos(xMin, yMin, zMin); Vec3i size = new Vec3i(Math.abs(dx), Math.abs(dy), Math.abs(dz)); structureTemplate.fillFromWorld( @@ -176,6 +227,9 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { ); CompoundTag newStructureCompoundTag = structureTemplate.save(new CompoundTag()); + + // Gather all existing block entity data for that same area of the world and append it to the + // exported CompoundTag from the structure template. ListTag blockList = newStructureCompoundTag.getList("blocks", Tag.TAG_COMPOUND); for (int i = 0; i < blockList.size(); i++) { CompoundTag tag = blockList.getCompound(i); @@ -196,22 +250,31 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { // Response header and response body Headers responseHeaders = httpExchange.getResponseHeaders(); + addDefaultResponseHeaders(responseHeaders); if (returnPlainText) { - responseHeaders.add("Content-Type", "text/plain; charset=UTF-8"); + addResponseHeadersContentTypePlain(responseHeaders); + responseString = newStructureCompoundTag.toString(); resolveRequest(httpExchange, responseString); } else if (returnJson) { + addResponseHeadersContentTypeJson(responseHeaders); + JsonObject tagsAsJsonObject = JsonParser.parseString(new JsonTagVisitor().visit(newStructureCompoundTag)).getAsJsonObject(); responseString = new Gson().toJson(tagsAsJsonObject); - responseHeaders.add("Content-Type", "application/json; charset=UTF-8"); resolveRequest(httpExchange, responseString); } else { - responseHeaders.add("Content-Type", "application/octet-stream"); + addResponseHeadersContentTypeBinary(responseHeaders); + ByteArrayOutputStream boas = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(boas); - NbtIo.writeCompressed(newStructureCompoundTag, dos); + if (returnCompressed) { + responseHeaders.add("Content-Encoding", "gzip"); + NbtIo.writeCompressed(newStructureCompoundTag, dos); + } else { + NbtIo.write(newStructureCompoundTag, dos); + } dos.flush(); byte[] responseBytes = boas.toByteArray(); @@ -219,7 +282,7 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { } } else { - throw new HandlerBase.HttpException("Method not allowed. Only POST and GET requests are supported.", 405); + throw new HttpException("Method not allowed. Only POST and GET requests are supported.", 405); } } }