From e4302858874786012b35f04e32649835382446a4 Mon Sep 17 00:00:00 2001 From: Niels-NTG Date: Sun, 12 Feb 2023 11:44:10 +0100 Subject: [PATCH 1/2] Remove support from JSON-formatted NBT-data in favour of SNBT --- docs/Endpoints.md | 35 ++---- docs/schema.blocks.get.json | 22 +--- docs/schema.blocks.put.json | 22 +--- docs/schema.entities.get.json | 26 +--- docs/schema.entities.patch.json | 12 +- docs/schema.entities.put.json | 11 +- .../handlers/BlocksHandler.java | 26 +--- .../handlers/ChunkHandler.java | 4 +- .../handlers/EntitiesHandler.java | 57 ++++----- .../handlers/StructureHandler.java | 15 +-- .../utils/JsonTagVisitor.java | 113 ------------------ 11 files changed, 55 insertions(+), 288 deletions(-) delete mode 100644 src/main/java/com/gdmc/httpinterfacemod/utils/JsonTagVisitor.java diff --git a/docs/Endpoints.md b/docs/Endpoints.md index 8f105d2..baaa09f 100644 --- a/docs/Endpoints.md +++ b/docs/Endpoints.md @@ -481,10 +481,10 @@ Read [chunks](https://minecraft.fandom.com/wiki/Chunk) within a given range and ## Request headers -| key | valid values | defaults to | description | -|-----------------|--------------------------------------------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain`, `application/octet-stream` | `application/octet-stream` | Response data type. By default returns as raw bytes of a [NBT](https://minecraft.fandom.com/wiki/NBT_format) file. Use `text/plain` for the same data, but in the human-readable [SNBT](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) format. And use `application/json` for better readable data at the cost of losing some data type precision, refer to [JSON and NBT](https://minecraft.fandom.com/wiki/NBT_format#Conversion_from_JSON) for more information. | -| Accept-Encoding | `gzip`, `*` | `*` | If set to `gzip`, any raw bytes NBT file is compressed using GZIP. | +| key | valid values | defaults to | description | +|-----------------|------------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Accept | `text/plain`, `application/octet-stream` | `application/octet-stream` | Response data type. By default returns as raw bytes of a [NBT](https://minecraft.fandom.com/wiki/NBT_format) file. Use `text/plain` for the same data, but in the human-readable [SNBT](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) format. | +| Accept-Encoding | `gzip`, `*` | `*` | If set to `gzip`, any raw bytes NBT file is compressed using GZIP. | ## Request body @@ -586,10 +586,10 @@ Create an [NBT](https://minecraft.fandom.com/wiki/NBT_format) structure file fro ## Request headers -| key | valid values | defaults to | description | -|-----------------|--------------------------------------------------------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain`, `application/octet-stream` | `application/octet-stream` | Response data type. By default returns the contents that makes a real NBT file. Use `text/plain` for a more human readable lossless version of the data in the [SNBT](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) format, and `application/json` for better readable data at the cost of losing some data type precision, refer to [JSON and NBT](https://minecraft.fandom.com/wiki/NBT_format#Conversion_from_JSON) for more information. | -| Accept-Encoding | `gzip`, `*` | `gzip` | If set to `gzip`, compress resulting file using gzip compression. | +| key | valid values | defaults to | description | +|-----------------|------------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Accept | `text/plain`, `application/octet-stream` | `application/octet-stream` | Response data type. By default returns the contents that makes a real NBT file. Use `text/plain` for a more human readable lossless version of the data in the [SNBT](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) format. | +| Accept-Encoding | `gzip`, `*` | `gzip` | If set to `gzip`, compress resulting file using gzip compression. | ## Request body @@ -729,30 +729,21 @@ For placing a red cat that's invulnerable and permanently on fire, reproduction "x": "~2", "y": "~", "z": "~-1", - "data": { - "variant": "minecraft:red", - "Invulnerable": true, - "HasVisualFire": true - } + "data": "{variant:\"minecraft:red\",Invulnerable: true,HasVisualFire: true}" }, { "id": "minecraft:painting", "x": "~-1", "y": 68, "z": "~2", - "data": { - "Facing": 2, - "variant": "wanderer" - } + "data": "{Facing:2,variant:\"wanderer\"}" }, { "id": "minecraft:zombie", "x": "~1", "y": "~", "z": "~-4", - "data": { - "CanBreakDoors": true - } + "data": "{CanBreakDoors:true}" } ] ``` @@ -816,9 +807,7 @@ When changing a black cat with UUID `"475fb218-68f1-4464-8ac5-e559afd8e00d"` (ob [ { "uuid": "475fb218-68f1-4464-8ac5-e559afd8e00d", - "data": { - "variant": "minecraft:red" - } + "data": "{variant:\"minecraft:red\"}" } ] ``` diff --git a/docs/schema.blocks.get.json b/docs/schema.blocks.get.json index b531e2f..6bcedff 100644 --- a/docs/schema.blocks.get.json +++ b/docs/schema.blocks.get.json @@ -41,27 +41,11 @@ "default": null }, "data": { - "type": "object", + "type": "string", "title": "Block Entity Data", - "description": "Object containing block entity data (https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) information. This is only included if URL parameter `includeData=true` is present.", + "description": "SNBT-formatted string (https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) containing block entity data (https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) information. This is only included if URL parameter `includeData=true` is present.", "examples": [ - { - "Items": [ - { - "Count": 48, - "Slot": 0, - "id": "minecraft:lantern" - }, - { - "Count": 1, - "Slot": 1, - "id": "minecraft:golden_axe", - "tag": { - "Damage": 0 - } - } - ] - } + "{Items:[{Count:48b,Slot:0b,id:\"minecraft:lantern\"},{Count:1b,Slot:1b,id:\"minecraft:golden_axe\",tag:{Damage:0}}]}" ], "default": null } diff --git a/docs/schema.blocks.put.json b/docs/schema.blocks.put.json index a65a388..7c44640 100644 --- a/docs/schema.blocks.put.json +++ b/docs/schema.blocks.put.json @@ -41,27 +41,11 @@ "default": null }, "data": { - "type": "object", + "type": "string", "title": "Block Entity Data", - "description": "Object containing block entity data (https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) information", + "description": "SNBT-formatted string (https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) containing block entity data (https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) information", "examples": [ - { - "Items": [ - { - "Count": 48, - "Slot": 0, - "id": "minecraft:lantern" - }, - { - "Count": 1, - "Slot": 1, - "id": "minecraft:golden_axe", - "tag": { - "Damage": 0 - } - } - ] - } + "{Items:[{Count:48b,Slot:0b,id:\"minecraft:lantern\"},{Count:1b,Slot:1b,id:\"minecraft:golden_axe\",tag:{Damage:0}}]}" ], "default": null } diff --git a/docs/schema.entities.get.json b/docs/schema.entities.get.json index ba49d27..d42e024 100644 --- a/docs/schema.entities.get.json +++ b/docs/schema.entities.get.json @@ -14,20 +14,12 @@ "format": "uuid" }, "data": { - "type": "object", + "type": "string", "title": "Entity Data", - "description": "Object containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information. This is only included if URL parameter `includeData=true` is present.", + "description": "SNBT string (https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information. This is only included if URL parameter `includeData=true` is present. Example data is truncated, real entity data is often much bigger.", "examples": [ - { - "Pos": [ - 197.50701490860138, - -2.0, - 35.512435215238455 - ], - "variant": "minecraft:red", - "HasVisualFire": true, - "Invulnerable": true - } + "{AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}]}", + "{OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[-16.675752318023665d,1.0d,-13.779639264516527d],RestocksToday:0,Rotation:[324.41544f,0.0f]}" ] } }, @@ -35,16 +27,6 @@ "uuid" ], "title": "Entity Information Element" - }, - "PosAxis": { - "type": "integer", - "title": "Position Axis", - "description": "Describe x/y/z absolute position of entity", - "examples": [ - -2, - 781, - 97.44 - ] } } } diff --git a/docs/schema.entities.patch.json b/docs/schema.entities.patch.json index f6a7017..eb57c42 100644 --- a/docs/schema.entities.patch.json +++ b/docs/schema.entities.patch.json @@ -9,21 +9,17 @@ "type": "object", "additionalProperties": false, "properties": { - "uuid" { + "uuid": { "type": "string", "title": "UUID", "description": "Universally unique identifier (https://minecraft.fandom.com/wiki/Universally_unique_identifier) of an entity in the world" }, "data": { - "type": "object", + "type": "string", "title": "Entity Data", - "description": "Object containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information", + "description": "SNBT-formatted string (https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information that should be changed", "examples": [ - { - "variant": "minecraft:red", - "HasVisualFire": true, - "Invulnerable": true - } + "{variant:\"minecraft:red\",hasVisualFire:true,Invulnerable:true}" ] } }, diff --git a/docs/schema.entities.put.json b/docs/schema.entities.put.json index a80dbc0..d4fb1ae 100644 --- a/docs/schema.entities.put.json +++ b/docs/schema.entities.put.json @@ -29,20 +29,15 @@ "$ref": "#/definitions/PosAxis" }, "data": { - "type": "object", + "type": "string", "title": "Entity Data", - "description": "Object containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information", + "description": "SNBT-formatted string (https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) containing entity data (https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) information", "examples": [ - { - "variant": "minecraft:red", - "HasVisualFire": true, - "Invulnerable": true - } + "{variant:\"minecraft:red\",hasVisualFire:true,Invulnerable:true}" ] } }, "required": [ - "data", "id", "x", "y", diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java index a91dd15..f3587f6 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java @@ -1,6 +1,5 @@ package com.gdmc.httpinterfacemod.handlers; -import com.gdmc.httpinterfacemod.utils.JsonTagVisitor; import com.google.gson.*; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; @@ -211,8 +210,8 @@ private String putBlocksHandler(InputStream requestBody, boolean parseRequestAsJ // 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()); + if (blockPlacementItem.has("data") && blockPlacementItem.get("data").isJsonPrimitive()) { + compoundTag = TagParser.parseTag(blockPlacementItem.get("data").getAsString()); } // Attempt to place block in the world. @@ -305,7 +304,7 @@ private String getBlocksHandler(boolean returnJson) { json.add("state", getBlockStateAsJsonObject(blockPos)); } if (includeData) { - json.add("data", getBlockDataAsJsonObject(blockPos)); + json.addProperty("data", getBlockDataAsStr(blockPos)); } jsonArray.add(json); } @@ -457,25 +456,6 @@ private String getBlockStateAsStr(BlockPos pos) { ']'; } - /** - * @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(); - BlockEntity blockEntity = serverLevel.getExistingBlockEntity(pos); - if (blockEntity != null) { - CompoundTag tags = blockEntity.saveWithoutMetadata(); - String tagsAsJsonString = (new JsonTagVisitor()).visit(tags); - JsonObject tagsAsJsonObject = JsonParser.parseString(tagsAsJsonString).getAsJsonObject(); - if (tagsAsJsonObject != null) { - return tagsAsJsonObject; - } - } - 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. diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java index 78051e3..d54cb1f 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java @@ -1,7 +1,5 @@ package com.gdmc.httpinterfacemod.handlers; -import com.gdmc.httpinterfacemod.utils.JsonTagVisitor; -import com.google.gson.JsonParser; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import net.minecraft.nbt.CompoundTag; @@ -113,7 +111,7 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { } if (returnJson) { - String responseString = JsonParser.parseString((new JsonTagVisitor()).visit(bodyNBT)).toString(); + String responseString = bodyNBT.toString(); setResponseHeadersContentTypeJson(responseHeaders); resolveRequest(httpExchange, responseString); diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java index f0dfda1..c382c55 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java @@ -1,6 +1,5 @@ package com.gdmc.httpinterfacemod.handlers; -import com.gdmc.httpinterfacemod.utils.JsonTagVisitor; import com.gdmc.httpinterfacemod.utils.TagMerger; import com.google.gson.*; import com.mojang.brigadier.StringReader; @@ -27,7 +26,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; public class EntitiesHandler extends HandlerBase { @@ -201,7 +204,7 @@ private String getEntitiesHandler(boolean returnJson) { JsonObject json = new JsonObject(); json.addProperty("uuid", entity.getStringUUID()); if (includeData) { - json.add("data", getEntityDataAsJsonObject(entity)); + json.addProperty("data", getEntityDataAsStr(entity)); } jsonArray.add(json); } @@ -299,7 +302,7 @@ private String patchEntitiesHandler(InputStream requestBody, boolean parseReques PatchEntityInstruction patchEntityInstruction; try { patchEntityInstruction = new PatchEntityInstruction(json); - } catch (IllegalArgumentException | CommandSyntaxException e) { + } catch (IllegalArgumentException | CommandSyntaxException | UnsupportedOperationException e) { returnValues.add(e.getMessage()); continue; } @@ -343,19 +346,6 @@ private String patchEntitiesHandler(InputStream requestBody, boolean parseReques return String.join("\n", returnValues); } - private JsonObject getEntityDataAsJsonObject(Entity entity) { - JsonObject json = new JsonObject(); - CompoundTag tags = entity.serializeNBT(); - if (tags != null) { - String tagAsJsonString = (new JsonTagVisitor()).visit(tags); - JsonObject tagsAsJsonObject = JsonParser.parseString(tagAsJsonString).getAsJsonObject(); - if (tagsAsJsonObject != null) { - return tagsAsJsonObject; - } - } - return json; - } - private String getEntityDataAsStr(Entity entity) { if (!includeData) { return ""; @@ -373,26 +363,15 @@ private final static class SummonEntityInstruction { private Vec3 entityPosition; private CompoundTag entityData; - SummonEntityInstruction(JsonObject inputData, CommandSourceStack commandSourceStack) throws CommandSyntaxException { + SummonEntityInstruction(JsonObject summonInstructionInput, CommandSourceStack commandSourceStack) throws CommandSyntaxException { String positionArgumentString = ""; - if (inputData.has("x") && inputData.has("y") && inputData.has("z")) { - positionArgumentString = inputData.get("x").getAsString() + " " + inputData.get("y").getAsString() + " " + inputData.get("z").getAsString(); - inputData.remove("x"); - inputData.remove("y"); - inputData.remove("z"); - } else if (inputData.has("Pos") && inputData.get("Pos").isJsonArray()) { - JsonArray positionInputData = inputData.get("Pos").getAsJsonArray(); - if (positionInputData.size() == 3) { - positionArgumentString = "%s %s %s".formatted( - positionInputData.get(0).getAsString(), - positionInputData.get(1).getAsString(), - positionInputData.get(2).getAsString() - ); - } + if (summonInstructionInput.has("x") && summonInstructionInput.has("y") && summonInstructionInput.has("z")) { + positionArgumentString = summonInstructionInput.get("x").getAsString() + " " + summonInstructionInput.get("y").getAsString() + " " + summonInstructionInput.get("z").getAsString(); } - String entitySummonArgumentString = inputData.has("id") ? inputData.get("id").getAsString() : ""; + String entityIDString = summonInstructionInput.has("id") ? summonInstructionInput.get("id").getAsString() : ""; + String entityDataString = summonInstructionInput.has("data") ? summonInstructionInput.get("data").getAsString() : ""; - parse(positionArgumentString + " " + entitySummonArgumentString + " " + inputData, commandSourceStack); + parse(positionArgumentString + " " + entityIDString + " " + entityDataString, commandSourceStack); } SummonEntityInstruction(String inputData, CommandSourceStack commandSourceStack) throws CommandSyntaxException { @@ -409,7 +388,11 @@ private void parse(String inputData, CommandSourceStack commandSourceStack) thro sr.skip(); try { - entityData = TagParser.parseTag(sr.getRemaining()); + String entityDataString = sr.getRemaining(); + if (entityDataString.isBlank()) { + entityDataString = "{}"; + } + entityData = TagParser.parseTag(entityDataString); } catch (StringIndexOutOfBoundsException e) { entityData = new CompoundTag(); } @@ -448,9 +431,9 @@ private final static class PatchEntityInstruction { private final UUID uuid; private final CompoundTag patchData; - PatchEntityInstruction(JsonObject inputData) throws IllegalArgumentException, CommandSyntaxException { + PatchEntityInstruction(JsonObject inputData) throws IllegalArgumentException, CommandSyntaxException, UnsupportedOperationException { uuid = UUID.fromString(inputData.get("uuid").getAsString()); - patchData = TagParser.parseTag(inputData.get("data").toString()); + patchData = TagParser.parseTag(inputData.get("data").getAsString()); } PatchEntityInstruction(String inputData) throws IllegalArgumentException, CommandSyntaxException { diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java index 8192cb8..711bd8f 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java @@ -1,9 +1,6 @@ package com.gdmc.httpinterfacemod.handlers; -import com.gdmc.httpinterfacemod.utils.JsonTagVisitor; import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import net.minecraft.core.BlockPos; @@ -127,7 +124,7 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { // (both default) compress the result using GZIP before sending out the response. String acceptEncodingHeader = getHeader(requestHeaders, "Accept-Encoding", "gzip"); boolean returnCompressed = acceptEncodingHeader.equals("gzip"); - getStructureHandler(httpExchange, returnPlainText, returnJson, returnCompressed); + getStructureHandler(httpExchange, returnPlainText, returnCompressed); } default -> throw new HttpException("Method not allowed. Only POST and GET requests are supported.", 405); } @@ -228,7 +225,7 @@ private void postStructureHandler(HttpExchange httpExchange, boolean parseReques resolveRequest(httpExchange, responseString); } - private void getStructureHandler(HttpExchange httpExchange, boolean returnPlainText, boolean returnJson, boolean returnCompressed) throws IOException { + private void getStructureHandler(HttpExchange httpExchange, boolean returnPlainText, boolean returnCompressed) throws IOException { // Calculate boundaries of area of blocks to gather information on. int xOffset = x + dx; int xMin = Math.min(x, xOffset); @@ -284,14 +281,6 @@ private void getStructureHandler(HttpExchange httpExchange, boolean returnPlainT return; } - if (returnJson) { - JsonObject tagsAsJsonObject = JsonParser.parseString(new JsonTagVisitor().visit(newStructureCompoundTag)).getAsJsonObject(); - - setResponseHeadersContentTypeJson(responseHeaders); - resolveRequest(httpExchange, new Gson().toJson(tagsAsJsonObject)); - return; - } - setResponseHeadersContentTypeBinary(responseHeaders, returnCompressed); ByteArrayOutputStream boas = new ByteArrayOutputStream(); diff --git a/src/main/java/com/gdmc/httpinterfacemod/utils/JsonTagVisitor.java b/src/main/java/com/gdmc/httpinterfacemod/utils/JsonTagVisitor.java deleted file mode 100644 index b247ac2..0000000 --- a/src/main/java/com/gdmc/httpinterfacemod/utils/JsonTagVisitor.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.gdmc.httpinterfacemod.utils; - -import net.minecraft.nbt.*; -import com.google.common.collect.Lists; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -public class JsonTagVisitor implements TagVisitor { - private final StringBuilder builder = new StringBuilder(); - private static final Pattern SIMPLE_VALUE = Pattern.compile("[A-Za-z0-9._+-]+"); - - public String visit(Tag tag) { - tag.accept(this); - return this.builder.toString(); - } - - public void visitString(StringTag stringTag) { - this.builder.append(StringTag.quoteAndEscape(stringTag.getAsString())); - } - - public void visitByte(ByteTag byteTag) { - this.builder.append(byteTag.getAsNumber()); - } - - public void visitShort(ShortTag shortTag) { - this.builder.append(shortTag.getAsNumber()); - } - - public void visitInt(IntTag intTag) { - this.builder.append(intTag.getAsNumber()); - } - - public void visitLong(LongTag longTag) { - this.builder.append(longTag.getAsNumber()); - } - - public void visitFloat(FloatTag floatTag) { - this.builder.append(floatTag.getAsFloat()); - } - - public void visitDouble(DoubleTag doubleTag) { - this.builder.append(doubleTag.getAsDouble()); - } - - public void visitByteArray(ByteArrayTag byteTags) { - this.builder.append('['); - byte[] byteArray = byteTags.getAsByteArray(); - for(int i = 0; i < byteArray.length; i++) { - if (i != 0) { - this.builder.append(','); - } - this.builder.append(byteArray[i]); - } - this.builder.append(']'); - } - - public void visitIntArray(IntArrayTag intTags) { - this.builder.append('['); - int[] intArray = intTags.getAsIntArray(); - for(int i = 0; i < intArray.length; i++) { - if (i != 0) { - this.builder.append(','); - } - this.builder.append(intArray[i]); - } - this.builder.append(']'); - } - - public void visitLongArray(LongArrayTag longTags) { - this.builder.append('['); - long[] longArray = longTags.getAsLongArray(); - for (int i = 0; i < longArray.length; i++) { - if (i != 0) { - this.builder.append(','); - } - this.builder.append(longArray[i]); - } - this.builder.append(']'); - } - - public void visitList(ListTag listTag) { - this.builder.append('['); - for(int i = 0; i < listTag.size(); i++) { - if (i != 0) { - this.builder.append(','); - } - - this.builder.append((new JsonTagVisitor()).visit(listTag.get(i))); - } - this.builder.append(']'); - } - - public void visitCompound(CompoundTag compoundTag) { - this.builder.append('{'); - List list = Lists.newArrayList(compoundTag.getAllKeys()); - Collections.sort(list); - - for (String s : list) { - if (this.builder.length() != 1) { - this.builder.append(','); - } - this.builder.append(handleEscape(s)).append(':').append((new JsonTagVisitor()).visit(compoundTag.get(s))); - } - this.builder.append('}'); - } - - private static String handleEscape(String s) { - return SIMPLE_VALUE.matcher(s).matches() ? s : StringTag.quoteAndEscape(s); - } - - public void visitEnd(EndTag endTag) {} -} From 749e0260e88464d2b1095c6c760979838509e4a7 Mon Sep 17 00:00:00 2001 From: Niels-NTG Date: Sun, 12 Feb 2023 22:02:22 +0100 Subject: [PATCH 2/2] Remove plain-text request and response bodies in favour of JSON format + better status/error messages --- CHANGELOG.md | 8 + build.gradle | 2 +- docs/Endpoints.md | 571 ++++++++---------- .../handlers/BiomesHandler.java | 77 +-- .../handlers/BlocksHandler.java | 261 +++----- .../handlers/BuildAreaHandler.java | 1 - .../handlers/ChunkHandler.java | 11 +- .../handlers/CommandHandler.java | 27 +- .../handlers/EntitiesHandler.java | 252 +++----- .../handlers/HandlerBase.java | 35 +- .../handlers/StructureHandler.java | 21 +- 11 files changed, 480 insertions(+), 786 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f18f57b..9d68bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# GDMC-HTTP 1.0.0 (Minecraft 1.19.2) +- BREAKING: JSON-formatted NBT-like data is no longer supported in request bodies. Use [SNBT notation](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) instead. +- BREAKING: Properties containing NBT values in JSON responses are no longer formatted as JSON, but as [SNBT strings](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format). +- BREAKING: Plain-text formatted responses have been removed in favour of JSON. +- BREAKING: Consistent error messages. +- BREAKING: Plain-text request bodies are no longer accepted (except for `POST /command`). JSON-formatted request bodies are expected instead. +- FIX: Improved performance! + # GDMC-HTTP 0.7.6 (Minecraft 1.19.2) - FIX: `GET /biomes` now returns an empty string for the biome ID if the requested position is outside of the vertical boundaries of the world. - FIX: Typo in error message `POST /structure` handler. diff --git a/build.gradle b/build.gradle index 9b9a8b3..3efad5d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'net.minecraftforge.gradle' version '5.1.+' } -version = '0.7.6' +version = '1.0.0' group = 'com.nilsgawlik.gdmchttp' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = 'gdmchttp' diff --git a/docs/Endpoints.md b/docs/Endpoints.md index baaa09f..75c6fa8 100644 --- a/docs/Endpoints.md +++ b/docs/Endpoints.md @@ -16,15 +16,23 @@ The following error codes can occur at any endpoint: - `405`: "Method not allowed" - `500`: "Internal server error" +## Request headers + +The requests headers for all endpoints are the followingen, unless stated otherwise. + +| key | valid values | defaults to | description | +|--------------|--------------------|--------------------|-----------------------------------------------------------------------------------------------------------| +| Content-Type | `application/json` | `application/json` | Request body content type is expected to have correct JSON formatting. Otherwise a `400` error is thrown. | + ## Response headers The responses for all endpoints return with the following headers, unless stated otherwise. -| key | value | description | -|-----------------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------| -| Access-Control-Allow-Origin | `*` | | -| Content-Disposition | `inline` | | -| Content-Type | `text/plain; charset=UTF-8` | If the `Accept: application/json` is present in the request header, the value will be `application/json; charset=UTF-8` instead. | +| key | value | description | +|-----------------------------|-----------------------------------|-------------| +| Access-Control-Allow-Origin | `*` | | +| Content-Disposition | `inline` | | +| Content-Type | `application/json; charset=UTF-8` | | # Send Commands `POST /commands` @@ -38,7 +46,9 @@ Send one or more Minecraft console commands to the server. For the full list of ## Request headers -None +| key | value | description | +|-----------------------------|-----------------------------|-------------| +| Content-Type | `text/plain; charset=UTF-8` | | ## Request body @@ -50,7 +60,7 @@ The request body should be formatted as plain-text and can contain multiple comm ## Response body -A plain-text response, where the result of each command is displayed on separate lines. +A JSON array with an entry on the result of each command. ## Example @@ -66,22 +76,49 @@ say end each command will be executed line by line in the context of the overworld dimension. When complete a response is returned with return values for each command on separate lines. A return value can either be an integer or an error message. For example the request above might return: -``` -1 -1 -1 -289 -1 +```json +[ + { + "status": 1 + }, + { + "status": 1 + }, + { + "status": 1 + }, + { + "status": 1, + "message": "289" + }, + { + "status": 1 + } +] ``` And on a subsequent call, two of the commands will fail, so the return text will be: -``` -1 -1 -Could not set the block -No blocks were filled -1 +```json +[ + { + "status": 1 + }, + { + "status": 1 + }, + { + "status": 0, + "message": "Could not set the block" + }, + { + "status": 0, + "message": "No blocks were filled" + }, + { + "status": 1 + } +] ``` # Read blocks `GET /blocks` @@ -104,9 +141,7 @@ Get information for one or more blocks in a given area. ## Request headers -| key | valid values | defaults to | description | -|--------|----------------------------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type. If set to `application/json`, response is formatted as a JSON array with objects describing each block. If not, a plain-text with each block description on a separate line | +[Default](#Request-headers) ## Request body @@ -118,107 +153,113 @@ N/A ## Response body -### JSON format - -If request header has `Accept: application/json` set, the response follows this [schema](./schema.blocks.get.json). - -### Plain-text format - -If request has request header has `Accept: text/plain`, it will return a list of blocks, each on a separate line. - -``` - []{} -``` - -- `x`, `y`, `z`: block placement position. Should be negative or positive integer numbers indicating its absolute position and are always present. -- `id`: namespaced block ID. Always required. Examples: `minecraft:stone`, `minecraft:clay`, `minecraft:green_stained_glass`. -- `[blockState]`: If URL parameters have `includeState=true`, this part contains [block state](https://minecraft.fandom.com/wiki/Block_states) for the block, written inside square brackets. Example: `[facing=east,lit=false]`. -- `{blockData}`: If URL parameters have `includeData=true`, this part contains [block entity data](https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) for the block, written inside curly brackets. Example: `{Items:[{Count:64b,Slot:0b,id:"minecraft:iron_bars"},{Count:24b,Slot:1b,id:"minecraft:lantern"}]}` +Response body follows this [schema](./schema.blocks.get.json). ## Example -To get a the block at position x=28, y=67 and z=-73, request `GET /blocks?x=-417&y=63&z=303`, which could return: - -``` --417 63 303 minecraft:dirt -``` - -To get all block within a 2x2x2 area, request `GET /blocks?x=-417&y=63&z=303&dx=2&dy=2&dz=2`, which returns a list with each block on a seperate line: +To get a the block at position x=28, y=67 and z=-73, request `GET /blocks?x=5525&y=62&z=4381`, which could return: -``` --417 63 303 minecraft:dirt --417 63 304 minecraft:grass_block --417 64 303 minecraft:spruce_log --417 64 304 minecraft:air --416 63 303 minecraft:grass_block --416 63 304 minecraft:grass_block --416 64 303 minecraft:air --416 64 304 minecraft:air +```json +[ + { + "id": "minecraft:grass_block", + "x": 5525, + "y": 62, + "z": 4381 + } +] ``` -To include the [block state](https://minecraft.fandom.com/wiki/Block_states), request `GET /blocks?x=-417&y=64&z=303&includeState=true`: +To get all block within a 2x2x2 area, request `GET /blocks?x=5525&y=62&z=4381&dx=2&dy=2&dz=2`, which returns a list with each block on a seperate line: -``` --417 64 303 minecraft:spruce_log[axis=y] +```json +[ + { + "id": "minecraft:grass_block", + "x": 5525, + "y": 62, + "z": 4381 + }, + { + "id": "minecraft:dirt", + "x": 5525, + "y": 62, + "z": 4382 + }, + { + "id": "minecraft:air", + "x": 5525, + "y": 63, + "z": 4381 + }, + { + "id": "minecraft:birch_log", + "x": 5525, + "y": 63, + "z": 4382 + }, + { + "id": "minecraft:grass_block", + "x": 5526, + "y": 62, + "z": 4381 + }, + { + "id": "minecraft:grass_block", + "x": 5526, + "y": 62, + "z": 4382 + }, + { + "id": "minecraft:air", + "x": 5526, + "y": 63, + "z": 4381 + }, + { + "id": "minecraft:air", + "x": 5526, + "y": 63, + "z": 4382 + } +] ``` -To get a JSON-formatted response, set `Accept: application/json` in the request header: +To include the [block state](https://minecraft.fandom.com/wiki/Block_states), request `GET /blocks?x=5525&y=64&z=4382&includeState=true`: ```json [ - { - "id": "minecraft:spruce_log", - "x": -417, - "y": 64, - "z": 303, - "state": { - "axis": "y" - } - } + { + "id": "minecraft:birch_log", + "x": 5525, + "y": 64, + "z": 4382, + "state": { + "axis": "y" + } + } ] ``` -To get information such as the contents of a chest, use `includeData=true` as part of the request; `GET /blocks?x=-446&y=79&z=337&includeState=true&includeData=true`: +To get information such as the contents of a chest, use `includeData=true` as part of the request; `GET /blocks?x=-300y=66&z=26&includeState=true&includeData=true`: ```json [ - { - "id": "minecraft:chest", - "x": -446, - "y": 79, - "z": 337, - "state": { - "facing": "north", - "type": "single", - "waterlogged": "false" - }, - "data": { - "Items": [ - { - "Count": 5, - "Slot": 0, - "id": "minecraft:poppy" - }, - { - "Count": 1, - "Slot": 10, - "id": "minecraft:grindstone" - }, - { - "Count": 1, - "Slot": 14, - "id": "minecraft:copper_ore" - }, - { - "Count": 4, - "Slot": 26, - "id": "minecraft:wheat_seeds" - } - ] - } - } + { + "id": "minecraft:chest", + "x": -300, + "y": 66, + "z": 26, + "state": { + "facing": "west", + "type": "single", + "waterlogged": "false" + }, + "data": "{Items:[{Count:1b,Slot:0b,id:\"minecraft:flint_and_steel\",tag:{Damage:0}},{Count:3b,Slot:2b,id:\"minecraft:lantern\"},{Count:7b,Slot:4b,id:\"minecraft:dandelion\"}]}" + } ] ``` +Note that that block data such as the contents of a chest are formatted as an [SNBT string](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format). # Place blocks `PUT /blocks` @@ -269,85 +310,46 @@ doBlockUpdates=True, spawnDrops=True -> 0000011 ## Request headers -| key | valid values | defaults to | description | -|--------------|----------------------------------|--------------|------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type | -| Content-Type | `application/json`, `text/plain` | `text/plain` | Content type of request body | +[Default](#Request-headers) ## Request body -### JSON format - -If request has the header `Content-Type: application/json`, the response is expected to be valid JSON. It should be a single JSON array of JSON objects according to this [schema](./schema.blocks.put.json). +Request body should be a single JSON array of JSON objects according to this [schema](./schema.blocks.put.json). After receiving the request, GDMC-HTTP will first to attempt to parse the whole request body into valid JSON. If this fails it will return a response with HTTP status `400`. -### Plain-text format - -If request has the header `Content-Type: text/plain` it will parse the request body as a plain-text, with each block placement instruction on a new line. - -``` - []{} -``` - -- `x`, `y`, `z`: block placement position. Should be negative or positive integer numbers. Use the `~` or `^` prefix to make these values [relative]((https://minecraft.fandom.com/wiki/Coordinates#Relative_world_coordinates)) to the position set in the request URL. If all are omitted, the corresponding coordinates from the request URL are used instead. -- `id`: namespaced block ID. Always required. Examples: `minecraft:stone`, `minecraft:clay`, `minecraft:green_stained_glass`. -- `[blockState]`: Optional [block state](https://minecraft.fandom.com/wiki/Block_states) for this block, written inside square brackets. Example: `[facing=east,lit=false]`. -- `{blockData}`: Optional [block entity data](https://minecraft.fandom.com/wiki/Chunk_format#Block_entity_format) for this block, written inside curly brackets. Example: `{Items:[{Count:64b,Slot:0b,id:"minecraft:iron_bars"},{Count:24b,Slot:1b,id:"minecraft:lantern"}]}` - ## Response headers [Default](#Response-headers) ## Response body -For each placement instruction in the request, it returns a list with a `"1"` if placement was successful, a `"0"` of that specification is already at that position in the world, and an error code if something else went wrong such as a missing or invalid block ID, placement position, etc. - -If request header has `Accept: application/json`, these values are listed in a JSON array. Otherwise they are listed in plain-text, each on a separated line. In either format the order of these corresponds to the order the placement instruction was listed. +Returns a status for each block placement instruction given in the request body. The order of these corresponds to the order the placement instruction was listed. ## Example -For `PUT /blocks?x=-43&y=2&z=23` with the request header `Content-Type: application/json` and `Accept: application/json` and this request body: +We can place a chest containing a few items and a quartz block next to it by sending the following request body to `PUT /blocks?x=-43&y=2&z=23`: ```json [ - { - "id": "minecraft:chest", - "x": "~2", - "y": 0, - "z": -106, - "state": { - "facing": "east", - "type": "single", - "waterlogged": "false" - }, - "data": { - "Items": [ - { - "Count": 48, - "Slot": 0, - "id": "minecraft:lantern" - }, - { - "Count": 1, - "Slot": 1, - "id": "minecraft:golden_axe", - "tag": { - "Damage": 0 - } - } - ] - } - }, - { - "id": "minecraft:acacia_sapling", - "x": "~2", - "y": 0, - "z": -104, - "state": { - "stage": "0" - } - } + { + "id": "minecraft:chest", + "x": -55, + "y": "~2", + "z": 77, + "state": { + "facing": "east", + "type": "single", + "waterlogged": "false" + }, + "data": "{Items:[{Count:48b,Slot:0b,id:\"minecraft:lantern\"},{Count:1b,Slot:1b,id:\"minecraft:golden_axe\",tag:{Damage:0}}]}" + }, + { + "id": "minecraft:quartz_block", + "x": -56, + "y": "~2", + "z": 77 + } ] ``` @@ -355,12 +357,16 @@ This returns: ```json [ - "1", - "1" + { + "status": 1 + }, + { + "status": 1 + } ] ``` -Where each line corresponds to a placement instruction, where "1" indicates a success, "0" that a block of that type is already there and an error message if something else went wrong. +Where each entry corresponds to a placement instruction, where `"status": 1` indicates a success, `"status": 0` that a block of that type is already there. This zero status may also appear when something else went wrong, such as when an invalid block ID was given. In such cases there also be a `"message"` attribute with an error message. # Read biomes `GET /biomes` @@ -380,9 +386,7 @@ Get [biome](https://minecraft.fandom.com/wiki/Biome#List_of_biomes) information ## Request headers -| key | valid values | defaults to | description | -|--------|----------------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type. If set as `application/json`, response is formatted as a JSON array with objects describing each biome. If not, a plain-text with each biome description on a separate line. | +[Default](#Request-headers) ## Request body @@ -394,74 +398,50 @@ N/A ## Response body -### JSON format - -If request header has `Accept: application/json`, the response should follow this [schema](./schema.biomes.get.json). - -### Plain-text response - -If request has request header has `Accept: text/plain`, it will return a list of biomes, each on a separate line. - -``` - -``` - -- `x`, `y`, `z`: block position. Should be negative or positive integer numbers indicating its absolute position and are always present. -- `id`: namespaced biome ID. Always required. Examples: `minecraft:plains`, `minecraft:wooded_badlands`, `minecraft:dripstone_caves`. This value is empty if the requested position is outside of the vertical limits of the world. +The response should follow this [schema](./schema.biomes.get.json). ## Example For getting the biomes of a row of blocks, request `GET /biomes?x=2350&y=64&z=-77&dx=-6`: -``` -2344 64 -77 minecraft:river -2345 64 -77 minecraft:river -2346 64 -77 minecraft:river -2347 64 -77 minecraft:river -2348 64 -77 minecraft:river -2349 64 -77 minecraft:forest -``` - -Setting the request header with `Accept: application/json` returns this data in JSON format: - ```json [ - { - "id": "minecraft:river", - "x": 2344, - "y": 64, - "z": -77 - }, - { - "id": "minecraft:river", - "x": 2345, - "y": 64, - "z": -77 - }, - { - "id": "minecraft:river", - "x": 2346, - "y": 64, - "z": -77 - }, - { - "id": "minecraft:river", - "x": 2347, - "y": 64, - "z": -77 - }, - { - "id": "minecraft:river", - "x": 2348, - "y": 64, - "z": -77 - }, - { - "id": "minecraft:forest", - "x": 2349, - "y": 64, - "z": -77 - } + { + "id": "minecraft:river", + "x": 2344, + "y": 64, + "z": -77 + }, + { + "id": "minecraft:river", + "x": 2345, + "y": 64, + "z": -77 + }, + { + "id": "minecraft:river", + "x": 2346, + "y": 64, + "z": -77 + }, + { + "id": "minecraft:river", + "x": 2347, + "y": 64, + "z": -77 + }, + { + "id": "minecraft:river", + "x": 2348, + "y": 64, + "z": -77 + }, + { + "id": "minecraft:forest", + "x": 2349, + "y": 64, + "z": -77 + } ] ``` @@ -509,7 +489,7 @@ Response should be encoded as an [NBT](https://minecraft.fandom.com/wiki/NBT_for - `ChunkZ`: Same value as URL parameter z - `ChunkDX`: Same value as URL parameter dx - `ChunkDZ`: Same value as URL parameter dz -- `Chunks`: List of chunks, where each chunk is in the [NBT Chunk format](https://minecraft.fandom.com/wiki/Chunk_format#NBT_structure) encoded as raw NBT, SNBT or JSON. +- `Chunks`: List of chunks, where each chunk is in the [NBT Chunk format](https://minecraft.fandom.com/wiki/Chunk_format#NBT_structure) encoded as raw NBT or SNBT. ## Example @@ -544,7 +524,6 @@ Place an [NBT](https://minecraft.fandom.com/wiki/NBT_format) structure file into | key | valid values | defaults to | description | |------------------|----------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type | | Content-Encoding | `gzip`, `*` | `gzip` | If set to `gzip`, input NBT file is assumed to be compressed using GZIP. This is enabled by default since files generated by the [Structure Block](https://minecraft.fandom.com/wiki/Structure_Block) are compressed this way. Setting this header to `*` will make GDMC-HTTP attempt to parse the file as both a compressed and uncompressed file (in that order) and continue with the one that is valid, ideal for when it's unclear if the file is compressed or not. | ## Request body @@ -557,9 +536,7 @@ A valid [NBT file](https://minecraft.fandom.com/wiki/NBT_format). ## Response body -Contains a single `1` if the placement was successful or a `0` or an error message if not. - -If request header has `Accept: application/json` this value is contained in a JSON array. +Contains a single `{ "status": 1 }` if the placement was successful or a `{ "status": 0 }` if not. ## Example @@ -633,9 +610,7 @@ Endpoint for reading all [entities](https://minecraft.fandom.com/wiki/Entity) fr ## Request headers -| key | valid values | defaults to | description | -|--------|----------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type. If set to `application/json`, response is formatted as a JSON array with objects describing each entity. If not, a plain-text with each entity description on a separate line | +[Default](#Request-headers) ## Request body @@ -647,27 +622,24 @@ N/A ## Response body -### JSON format - -If request header has `Accept: application/json` set, the response follows this [schema](./schema.entities.get.json). - -### Plain-text format - -``` - {} -``` - -- `uuid`: [Universally unique identifier](https://minecraft.fandom.com/wiki/Universally_unique_identifier) of this entity. -- `{entityData}`: if URL includes `includeData=true`, this has [entity data](https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) for this entity, written inside curly brackets. Example: `{variant: "minecraft:red", HasVisualFire: true, Invulnerable: true}` +The response follows this [schema](./schema.entities.get.json). ## Example -Given a pit with 3 cats in it, the request `GET /entities?x=196&y=-2&z=33&dx=10&dy=10&dz=10` may return: -``` -61e86e73-bdf9-40a1-9c84-2583b915923a {AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}],Attributes:[{Base:0.08d,Name:"forge:entity_gravity"},{Base:0.0d,Name:"forge:step_height_addition"},{Base:0.30000001192092896d,Name:"minecraft:generic.movement_speed"}],Brain:{memories:{}},CanPickUpLoot:0b,CanUpdate:1b,CollarColor:14b,DeathTime:0s,FallDistance:0.0f,FallFlying:0b,Fire:-1s,ForcedAge:0,HandDropChances:[0.085f,0.085f],HandItems:[{},{}],Health:10.0f,HurtByTimestamp:0,HurtTime:0s,InLove:0,Invulnerable:0b,LeftHanded:0b,Motion:[0.0d,-0.0784000015258789d,0.0d],OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[196.41771845279456d,-2.0d,35.69134625529584d],Rotation:[264.3099f,0.0f],Sitting:1b,UUID:[I;1642622579,-1107738463,-1669061245,-1189768646],id:"minecraft:cat",variant:"minecraft:white"} -3654091e-c345-4b0e-b724-a432b84061c0 {AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}],Attributes:[{Base:0.08d,Name:"forge:entity_gravity"},{Base:0.0d,Name:"forge:step_height_addition"},{Base:0.30000001192092896d,Name:"minecraft:generic.movement_speed"}],Brain:{memories:{}},CanPickUpLoot:0b,CanUpdate:1b,CollarColor:14b,DeathTime:0s,FallDistance:0.0f,FallFlying:0b,Fire:-1s,ForcedAge:0,HandDropChances:[0.085f,0.085f],HandItems:[{},{}],Health:10.0f,HurtByTimestamp:0,HurtTime:0s,InLove:0,Invulnerable:0b,LeftHanded:0b,Motion:[0.05596905643219711d,-0.0784000015258789d,0.04245116800847979d],OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[198.4937389760431d,-2.0d,33.95112349404843d],Rotation:[207.49457f,0.0f],Sitting:1b,UUID:[I;911477022,-1018868978,-1222335438,-1203740224],id:"minecraft:cat",variant:"minecraft:all_black"} -7243531d-f7e2-4544-9b20-66e9ce048070 {AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}],Attributes:[{Base:0.08d,Name:"forge:entity_gravity"},{Base:0.0d,Name:"forge:step_height_addition"},{Base:0.30000001192092896d,Name:"minecraft:generic.movement_speed"}],Brain:{memories:{}},CanPickUpLoot:0b,CanUpdate:1b,CollarColor:14b,DeathTime:0s,FallDistance:0.0f,FallFlying:0b,Fire:-1s,ForcedAge:0,HandDropChances:[0.085f,0.085f],HandItems:[{},{}],Health:10.0f,HurtByTimestamp:0,HurtTime:0s,InLove:0,Invulnerable:0b,LeftHanded:0b,Motion:[-0.09271304794030778d,-0.0784000015258789d,0.0d],OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[197.71203932496857d,-2.0d,33.30000001192093d],Rotation:[62.355515f,0.0f],Sitting:0b,UUID:[I;1917014813,-136166076,-1692375319,-838565776],id:"minecraft:cat",variant:"minecraft:red"} +Given a pit with 3 cats in it, the request `GET /entities?x=305&y=65&z=26&dx=10&dy=10&dz=10&includeData=true` may return: +```json +[ + { + "uuid": "26a2bf9a-9dbf-492a-910b-516f4322f3f2", + "data": "{AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}],Attributes:[{Base:0.08d,Name:\"forge:entity_gravity\"},{Base:40.0d,Modifiers:[{Amount:-0.0076567387992512986d,Name:\"Random spawn bonus\",Operation:1,UUID:[I;868537497,-1023129007,-1268290039,-433935503]}],Name:\"minecraft:generic.follow_range\"},{Base:25.0d,Name:\"minecraft:generic.max_health\"},{Base:0.0d,Name:\"forge:step_height_addition\"},{Base:0.17499999701976776d,Name:\"minecraft:generic.movement_speed\"}],Brain:{memories:{}},Bred:0b,CanPickUpLoot:0b,CanUpdate:1b,ChestedHorse:0b,DeathTime:0s,DespawnDelay:39171,EatingHaystack:0b,FallDistance:0.0f,FallFlying:0b,Fire:-1s,ForcedAge:0,HandDropChances:[0.085f,0.085f],HandItems:[{},{}],Health:25.0f,HurtByTimestamp:0,HurtTime:0s,InLove:0,Invulnerable:0b,LeftHanded:0b,Motion:[0.0d,-0.0784000015258789d,0.0d],OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[-296.3192426279384d,67.0d,35.572736569528644d],Rotation:[91.85614f,0.0f],Strength:5,Tame:0b,Temper:0,UUID:[I;648200090,-1648408278,-1861529233,1126364146],Variant:2,id:\"minecraft:trader_llama\"}" + }, + { + "uuid": "58c392b0-9eee-4174-a807-3b975a2369f4", + "data": "{AbsorptionAmount:0.0f,Age:0,Air:300s,ArmorDropChances:[0.085f,0.085f,0.085f,0.085f],ArmorItems:[{},{},{},{}],Attributes:[{Base:0.08d,Name:\"forge:entity_gravity\"},{Base:16.0d,Modifiers:[{Amount:0.026007290323946414d,Name:\"Random spawn bonus\",Operation:1,UUID:[I;110803122,-1164752996,-1083557595,-449135232]}],Name:\"minecraft:generic.follow_range\"},{Base:0.0d,Name:\"forge:step_height_addition\"},{Base:0.699999988079071d,Name:\"minecraft:generic.movement_speed\"}],Brain:{memories:{}},CanPickUpLoot:0b,CanUpdate:1b,DeathTime:0s,DespawnDelay:39172,FallDistance:0.0f,FallFlying:0b,Fire:-1s,ForcedAge:0,HandDropChances:[0.085f,0.085f],HandItems:[{},{}],Health:20.0f,HurtByTimestamp:0,HurtTime:0s,Inventory:[],Invulnerable:0b,LeftHanded:0b,Motion:[0.0d,-0.0784000015258789d,0.0d],Offers:{Recipes:[{buy:{Count:1b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:5,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:2b,id:\"minecraft:small_dripleaf\"},specialPrice:0,uses:0,xp:1},{buy:{Count:5b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:8,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:1b,id:\"minecraft:birch_sapling\"},specialPrice:0,uses:0,xp:1},{buy:{Count:5b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:8,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:1b,id:\"minecraft:jungle_sapling\"},specialPrice:0,uses:0,xp:1},{buy:{Count:1b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:12,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:1b,id:\"minecraft:red_tulip\"},specialPrice:0,uses:0,xp:1},{buy:{Count:1b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:5,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:2b,id:\"minecraft:moss_block\"},specialPrice:0,uses:0,xp:1},{buy:{Count:6b,id:\"minecraft:emerald\"},buyB:{Count:1b,id:\"minecraft:air\"},demand:0,maxUses:6,priceMultiplier:0.05f,rewardExp:1b,sell:{Count:1b,id:\"minecraft:blue_ice\"},specialPrice:0,uses:0,xp:1}]},OnGround:1b,PersistenceRequired:0b,PortalCooldown:0,Pos:[-302.76158910022104d,66.0d,35.324351502361225d],Rotation:[124.32312f,0.0f],UUID:[I;1489212080,-1628552844,-1475921001,1512270324],id:\"minecraft:wandering_trader\"}" + } +] ``` +This area happens to contain a wandering trader and their trusty lama. # Create entities `PUT /entities` @@ -684,31 +656,14 @@ Endpoint for summoning any number of [entities](https://minecraft.fandom.com/wik ## Request headers -| key | valid values | defaults to | description | -|--------------|----------------------------------|--------------|------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type | -| Content-Type | `application/json`, `text/plain` | `text/plain` | Content type of request body | +[Default](#Request-headers) ## Request body -### JSON format - -If request has the header `Content-Type: application/json`, the response is expected to be valid JSON. It should be a single JSON array of JSON objects according to this [schema](./schema.entities.put.json). +The request body should be a single JSON array of JSON objects according to this [schema](./schema.entities.put.json). After receiving the request, GDMC-HTTP will first to attempt to parse the whole request body into valid JSON. If this fails it will return a response with HTTP status `400`. -### Plain-text format - -If request has the header `Content-Type: text/plain` it will parse the request body as a plain-text, with each entity placement instruction on a new line. - -``` - {} -``` - -- `x`, `y`, `z`: entity position. Should be negative or positive floating point numbers. Use the `~` or `^` prefix to make these values [relative]((https://minecraft.fandom.com/wiki/Coordinates#Relative_world_coordinates)) to the position set in the request URL. If all are omitted, the corresponding coordinates from the request URL are used instead. -- `id`: namespaced entity ID. Always required. Examples: `minecraft:cat`, `minecraft:cow`, `minecraft:painting`. -- `{entityData}`: Optional [entity data](https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) for this entity, written inside curly brackets. Example: `{variant: "minecraft:red", HasVisualFire: true, Invulnerable: true}` - ## Response headers [Default](#Response-headers) @@ -717,8 +672,6 @@ If request has the header `Content-Type: text/plain` it will parse the request b For each placement instruction in the request, it returns a list with a the entity's UUID if placement was successful or an error code if something else went wrong such as a missing or invalid entity ID or incorrectly formatted entity data. -If request header has `Accept: application/json`, these values are listed in a JSON array. Otherwise they are listed in plain-text, each on a separated line. In either format the order of these corresponds to the order the placement instruction was listed. - ## Example For placing a red cat that's invulnerable and permanently on fire, reproduction of the painting *Wanderer above the Sea of Fog* and zombie into the world: `PUT /entities?x=92&y=64&z=-394` with the request body: @@ -760,45 +713,25 @@ Endpoint for changing the properties of [entities](https://minecraft.fandom.com/ ## Request headers -| key | valid values | defaults to | description | -|--------------|----------------------------------|--------------|------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type | -| Content-Type | `application/json`, `text/plain` | `text/plain` | Content type of request body | +[Default](#Request-headers) ## Request body The submitted properties need to be of the same data type as the target entity. Any property with a mismatching data type will be skipped. See the documentation on the [Entity Format](https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) and entities of a specific type for an overview of properties and their data types. -### JSON format - -If request has the header `Content-Type: application/json`, the response is expected to be valid JSON. It should be a single JSON array of JSON objects according to this [schema](./schema.entities.patch.json). +The response is expected to be valid JSON. It should be a single JSON array of JSON objects according to this [schema](./schema.entities.patch.json). After receiving the request, GDMC-HTTP will first to attempt to parse the whole request body into valid JSON. If this fails it will return a response with HTTP status `400`. Refer to [the conversion from JSON table](https://minecraft.fandom.com/wiki/NBT_format#Conversion_from_JSON) to ensure data types of property values match that of the target entity. -### Plain-text format - -If request has the header `Content-Type: text/plain` it will parse the request body as a plain-text, with each entity placement instruction on a new line. - -``` - {} -``` - -- `uuid`: [Universally unique identifier](https://minecraft.fandom.com/wiki/Universally_unique_identifier) of this entity. -- `{entityData}`: Optional [entity data](https://minecraft.fandom.com/wiki/Entity_format#Entity_Format) for this entity, written inside curly brackets. Example: `{variant: "minecraft:red", HasVisualFire: true, Invulnerable: true}` - -Refer to the [NBT format data types table](https://minecraft.fandom.com/wiki/NBT_format#Data_types) for the correct [SNBT notation](https://minecraft.fandom.com/wiki/NBT_format#SNBT_format) to ensure data types of property values match that of the target entity. - ## Response headers [Default](#Response-headers) ## Response body -For each patch instruction in the request, it returns a list with a `"1"` if an existing entity with that UUID has been found *and* if the data has changed after the patch, `"0"` if no entity exists in the world with this UUID, `"0"` if the patch has no effect on the existing data and an error message if a invalid UUID or patch data has been submitted or if merging the data failed for some other reason. - -If request header has `Accept: application/json`, these values are listed in a JSON array. Otherwise they are listed in plain-text, each on a separated line. In either format the order of these corresponds to the order the placement instruction was listed. +For each patch instruction in the request, it returns a list with a `{ "status": 1 }` if an existing entity with that UUID has been found *and* if the data has changed after the patch. `{ "status": 0 }` if no entity exists in the world with this UUID, if the patch has no effect on the existing data or if a invalid UUID or patch data has been submitted or if merging the data failed for some other reason. ## Example @@ -824,44 +757,29 @@ Endpoint for remove one or more [entities](https://minecraft.fandom.com/wiki/Ent ## Request headers -| key | valid values | defaults to | description | -|--------------|----------------------------------|--------------|------------------------------| -| Accept | `application/json`, `text/plain` | `text/plain` | Response data type | -| Content-Type | `application/json`, `text/plain` | `text/plain` | Content type of request body | +[Default](#Request-headers) ## Request body -### JSON format - -If request has the header `Content-Type: application/json`, the response is expected to be valid JSON. It should be a single JSON array of string-formatted UUIDs. +The request body is expected to be valid JSON. It should be a single JSON array of string-formatted UUIDs. After receiving the request, GDMC-HTTP will first to attempt to parse the whole request body into valid JSON. If this fails it will return a response with HTTP status `400`. -### Plain-text format - -If request has the header `Content-Type: text/plain` it will parse the request body as a plain-text, with each entity placement instruction on a new line. - -``` - -``` - -- `uuid`: [Universally unique identifier](https://minecraft.fandom.com/wiki/Universally_unique_identifier) of this entity. - ## Response headers [Default](#Response-headers) ## Response body -For each patch instruction in the request, it returns a list with a `"1"` if an existing entity with that UUID has been found *and* and is able to be removed, `"0"` if no entity exists in the world with this UUID and an error message if a invalid UUID. - -If request header has `Accept: application/json`, these values are listed in a JSON array. Otherwise they are listed in plain-text, each on a separated line. In either format the order of these corresponds to the order the placement instruction was listed. +For each patch instruction in the request, it returns a list with a `{ "status": 1 }` if an existing entity with that UUID has been found *and* and is able to be removed, `{ "status": 0 }` if no entity exists in the world with this UUID and an error message if a invalid UUID. ## Example To remove a cat with UUID `"475fb218-68f1-4464-8ac5-e559afd8e00d"` (obtained using the [`GET /entities`](#read-entities-get-entities) endpoint): `DELETE /entities` with the request body: -``` -475fb218-68f1-4464-8ac5-e559afd8e00d +```json +[ + "475fb218-68f1-4464-8ac5-e559afd8e00d" +] ``` # Get build area `GET /buildarea` @@ -920,7 +838,9 @@ N/A ## Response headers -[Default](#Response-headers) +| key | value | description | +|-----------------------------|-----------------------------|-------------| +| Content-Type | `text/plain; charset=UTF-8` | | ## Response body @@ -928,4 +848,7 @@ Plain-text response with the Minecraft version number. ## Example -`GET /version` returns `"1.19.2"`. +`GET /version` returns: +``` +1.19.2 +``` diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java index db266d2..592bd46 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BiomesHandler.java @@ -1,6 +1,5 @@ package com.gdmc.httpinterfacemod.handlers; -import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.sun.net.httpserver.Headers; @@ -12,7 +11,6 @@ import net.minecraft.world.level.biome.Biome; import java.io.IOException; -import java.util.ArrayList; import java.util.Map; import java.util.Optional; @@ -54,14 +52,9 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { throw new HandlerBase.HttpException(message, 400); } - // 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; + JsonArray responseList = new JsonArray(); if (method.equals("get")) { ServerLevel serverLevel = getServerLevel(dimension); @@ -79,53 +72,28 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { 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); - Optional> biomeResourceKey = serverLevel.getBiome(blockPos).unwrapKey(); - if (biomeResourceKey.isEmpty()) { - continue; - } - String biomeName = ""; - if (!serverLevel.isOutsideBuildHeight(blockPos)) { - biomeName = biomeResourceKey.get().location().toString(); - } - JsonObject json = new JsonObject(); - json.addProperty("id", biomeName); - json.addProperty("x", rangeX); - json.addProperty("y", rangeY); - json.addProperty("z", rangeZ); - jsonArray.add(json); + // Create a JsonArray with JsonObject, each contain a key-value pair for + // the x, y, z position and the namespaced biome name. + 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); + Optional> biomeResourceKey = serverLevel.getBiome(blockPos).unwrapKey(); + if (biomeResourceKey.isEmpty()) { + continue; } - } - } - 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); - Optional> biomeResourceKey = serverLevel.getBiome(blockPos).unwrapKey(); - if (biomeResourceKey.isEmpty()) { - continue; - } - String biomeName = ""; - if (!serverLevel.isOutsideBuildHeight(blockPos)) { - biomeName = biomeResourceKey.get().location().toString(); - } - biomesList.add(rangeX + " " + rangeY + " " + rangeZ + " " + biomeName); + String biomeName = ""; + if (!serverLevel.isOutsideBuildHeight(blockPos)) { + biomeName = biomeResourceKey.get().location().toString(); } + JsonObject json = new JsonObject(); + json.addProperty("id", biomeName); + json.addProperty("x", rangeX); + json.addProperty("y", rangeY); + json.addProperty("z", rangeZ); + responseList.add(json); } } - responseString = String.join("\n", biomesList); } } else { throw new HttpException("Method not allowed. Only GET requests are supported.", 405); @@ -134,12 +102,7 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { // Response headers Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - if (returnJson) { - setResponseHeadersContentTypeJson(responseHeaders); - } else { - setResponseHeadersContentTypePlain(responseHeaders); - } - resolveRequest(httpExchange, responseString); + resolveRequest(httpExchange, responseList.toString()); } } diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java index f3587f6..e7ad928 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BlocksHandler.java @@ -30,17 +30,13 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import javax.annotation.Nullable; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; -import java.util.stream.Collectors; - public class BlocksHandler extends HandlerBase { @@ -111,21 +107,14 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { throw new HttpException("Could not parse query parameter: " + e.getMessage(), 400); } - // 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 responseString; + JsonArray responseObject; switch (httpExchange.getRequestMethod().toLowerCase()) { case "put" -> { - String contentTypeHeader = getHeader(requestHeaders, "Content-Type", "*/*"); - boolean parseRequestAsJson = hasJsonTypeInHeader(contentTypeHeader); - responseString = putBlocksHandler(httpExchange.getRequestBody(), parseRequestAsJson, returnJson); + responseObject = putBlocksHandler(httpExchange.getRequestBody()); } case "get" -> { - responseString = getBlocksHandler(returnJson); + responseObject = getBlocksHandler(); } default -> throw new HttpException("Method not allowed. Only PUT and GET requests are supported.", 405); } @@ -133,144 +122,96 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { // Response headers Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - if (returnJson) { - setResponseHeadersContentTypeJson(responseHeaders); - } else { - setResponseHeadersContentTypePlain(responseHeaders); - } - resolveRequest(httpExchange, responseString); + resolveRequest(httpExchange, responseObject.toString()); } /** * Place blocks any number of blocks into the world * * @param requestBody request body of block placement instructions - * @param parseRequestAsJson if true, treat input as JSON - * @param returnJson if true, return result as JSON-formatted string * @return block placement results */ - private String putBlocksHandler(InputStream requestBody, boolean parseRequestAsJson, boolean returnJson) { + private JsonArray putBlocksHandler(InputStream requestBody) { 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)); - ArrayList returnValues = new ArrayList<>(); + JsonArray returnValues = new JsonArray(); - if (parseRequestAsJson) { - JsonArray blockPlacementList; - try { - blockPlacementList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); - } catch (JsonSyntaxException jsonSyntaxException) { - throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); - } + JsonArray blockPlacementList; + try { + blockPlacementList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); + } catch (JsonSyntaxException jsonSyntaxException) { + throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); + } - for (JsonElement blockPlacement : blockPlacementList) { - String returnValue; - JsonObject blockPlacementItem = blockPlacement.getAsJsonObject(); - try { - - // 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(); - } - } + for (JsonElement blockPlacement : blockPlacementList) { + JsonObject blockPlacementItem = blockPlacement.getAsJsonObject(); + try { - // Pass block Id and block state string into a Stringreader with the the block state parser. - BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock( - getBlockRegisteryLookup(commandSourceStack), - 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") && blockPlacementItem.get("data").isJsonPrimitive()) { - compoundTag = TagParser.parseTag(blockPlacementItem.get("data").getAsString()); + // 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(instructionStatus(false, "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(); } - - // Attempt to place block in the world. - returnValue = setBlock(blockPos, blockState, compoundTag, blockFlags) + ""; - - } catch (CommandSyntaxException e) { - returnValue = e.getMessage(); } - returnValues.add(returnValue); - } - - } else { - List blockPlacementList = new BufferedReader(new InputStreamReader(requestBody)) - .lines().toList(); - - for (String blockPlacementItem : blockPlacementList) { - String returnValue; - StringReader sr = new StringReader(blockPlacementItem); - BlockPos blockPos; - try { - // Attempt to parse a block position from string. If no valid position is found, use the position of the URL query parameters instead. - try { - blockPos = getBlockPosFromString(sr, commandSourceStack); - } catch (CommandSyntaxException e1) { - blockPos = new BlockPos(x, y, z); - } - BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock( - getBlockRegisteryLookup(commandSourceStack), - sr, - true - ); - BlockState blockState = parsedBlockState.blockState(); - CompoundTag compoundTag = parsedBlockState.nbt(); - - returnValue = setBlock(blockPos, blockState, compoundTag, blockFlags) + ""; - - } catch (CommandSyntaxException e) { - returnValue = e.getMessage(); + // Pass block Id and block state string into a Stringreader with the the block state parser. + BlockStateParser.BlockResult parsedBlockState = BlockStateParser.parseForBlock( + getBlockRegisteryLookup(commandSourceStack), + 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") && blockPlacementItem.get("data").isJsonPrimitive()) { + compoundTag = TagParser.parseTag(blockPlacementItem.get("data").getAsString()); } - returnValues.add(returnValue); + // Attempt to place block in the world. + boolean isSuccess = setBlock(blockPos, blockState, compoundTag, blockFlags) == 1; + returnValues.add(instructionStatus(isSuccess)); + + } catch (CommandSyntaxException e) { + returnValues.add(instructionStatus(false, e.getMessage())); } } - // 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) { - return new Gson().toJson(returnValues); - } - return String.join("\n", returnValues); + return returnValues; } /** * Get information on one of more blocks in the world. * - * @param returnJson if true, return response in JSON format * @return list of block information */ - private String getBlocksHandler(boolean returnJson) { + private JsonArray getBlocksHandler() { // Calculate boundaries of area of blocks to gather information on. int xOffset = x + dx; @@ -285,54 +226,31 @@ private String getBlocksHandler(boolean returnJson) { 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, 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++) { - for (int rangeZ = zMin; rangeZ < zMax; rangeZ++) { - BlockPos blockPos = new BlockPos(rangeX, rangeY, rangeZ); - String blockId = getBlockAsStr(blockPos); - JsonObject json = new JsonObject(); - json.addProperty("id", blockId); - json.addProperty("x", rangeX); - json.addProperty("y", rangeY); - json.addProperty("z", rangeZ); - if (includeState) { - json.add("state", getBlockStateAsJsonObject(blockPos)); - } - if (includeData) { - json.addProperty("data", getBlockDataAsStr(blockPos)); - } - jsonArray.add(json); - } - } - } - return new Gson().toJson(jsonArray); - } - - // 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<>(); + // 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++) { for (int rangeZ = zMin; rangeZ < zMax; rangeZ++) { BlockPos blockPos = new BlockPos(rangeX, rangeY, rangeZ); - String listItem = rangeX + " " + rangeY + " " + rangeZ + " " + getBlockAsStr(blockPos); + String blockId = getBlockAsStr(blockPos); + JsonObject json = new JsonObject(); + json.addProperty("id", blockId); + json.addProperty("x", rangeX); + json.addProperty("y", rangeY); + json.addProperty("z", rangeZ); if (includeState) { - listItem += getBlockStateAsStr(blockPos); + json.add("state", getBlockStateAsJsonObject(blockPos)); } if (includeData) { - listItem += getBlockDataAsStr(blockPos); + json.addProperty("data", getBlockDataAsStr(blockPos)); } - responseList.add(listItem); + jsonArray.add(json); } } } - return String.join("\n", responseList); + return jsonArray; } private BlockState getBlockStateAtPosition(BlockPos pos) { @@ -418,7 +336,6 @@ private int setBlock(BlockPos pos, BlockState blockState, CompoundTag blockEntit neighbourBlockState.updateNeighbourShapes(serverLevel, neighbourPosition, flags); } } - return 1; } else { return 0; @@ -445,17 +362,6 @@ private JsonObject getBlockStateAsJsonObject(BlockPos pos) { 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) { - BlockState bs = getBlockStateAtPosition(pos); - return '[' + - bs.getValues().entrySet().stream().map(propertyToStringFunction).collect(Collectors.joining(",")) + - ']'; - } - /** * @param pos Position of block in the world. * @return {@link String} containing the block entity data of the block at the given position. @@ -506,23 +412,6 @@ public static int getBlockFlags(boolean doBlockUpdates, boolean spawnDrops) { 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' - private static final Function, Comparable>, String> propertyToStringFunction = - new Function<>() { - public String apply(@Nullable Map.Entry, Comparable> element) { - if (element == null) { - return ""; - } else { - Property property = element.getKey(); - return property.getName() + "=" + this.valueToName(property, element.getValue()); - } - } - - private > String valueToName(Property property, Comparable propertyValue) { - return property.getName((T) propertyValue); - } - }; - // function that converts a bunch of Property/Comparable pairs into String/String pairs private static final Function, Comparable>, Map.Entry> propertyToStringPairFunction = new Function<>() { diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java index 2e1d44d..43ab43d 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/BuildAreaHandler.java @@ -71,7 +71,6 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - setResponseHeadersContentTypeJson(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 d54cb1f..70470c7 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/ChunkHandler.java @@ -57,12 +57,11 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { throw new HttpException("Method not allowed. Only GET requests are supported.", 405); } - // Check if clients wants a response in plain-text or JSON format. If not, return response + // Check if clients wants a response in plain-text. 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, // compress the result using GZIP before sending out the response. @@ -110,14 +109,6 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { return; } - if (returnJson) { - String responseString = bodyNBT.toString(); - - setResponseHeadersContentTypeJson(responseHeaders); - resolveRequest(httpExchange, responseString); - return; - } - setResponseHeadersContentTypeBinary(responseHeaders, returnCompressed); ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java index b6b0796..ad8a1e9 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/CommandHandler.java @@ -1,13 +1,17 @@ package com.gdmc.httpinterfacemod.handlers; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.MinecraftServer; -import java.io.*; -import java.util.ArrayList; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -31,32 +35,33 @@ public void internalHandle(HttpExchange httpExchange) throws IOException { CommandSourceStack cmdSrc = createCommandSource("GDMC-CommandHandler", dimension); - List outputs = new ArrayList<>(); + JsonArray returnValues = new JsonArray(); for (String command: commands) { if (command.length() == 0) { continue; } // requests to run the actual command execution on the main thread - CompletableFuture cfs = CompletableFuture.supplyAsync(() -> { + CompletableFuture cfs = CompletableFuture.supplyAsync(() -> { try { - return "" + mcServer.getCommands().getDispatcher().execute(command, cmdSrc); + int commandStatus = mcServer.getCommands().getDispatcher().execute(command, cmdSrc); + return instructionStatus( + commandStatus != 0, + commandStatus != 1 && commandStatus != 0 ? String.valueOf(commandStatus) : null + ); } catch (CommandSyntaxException e) { - return e.getMessage(); + return instructionStatus(false, e.getMessage()); } }, mcServer); // block this thread until the above code has run on the main thread - String result = cfs.join(); - outputs.add(result); + returnValues.add(cfs.join()); } // Response headers Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - setResponseHeadersContentTypePlain(responseHeaders); // body - String responseString = String.join("\n", outputs); - resolveRequest(httpExchange, responseString); + resolveRequest(httpExchange, returnValues.toString()); } } \ No newline at end of file diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java index c382c55..9ab9b54 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/EntitiesHandler.java @@ -22,11 +22,9 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -74,30 +72,22 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { throw new HttpException(message, 400); } - // 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 contentTypeHeader = getHeader(requestHeaders, "Content-Type", "*/*"); - boolean parseRequestAsJson = hasJsonTypeInHeader(contentTypeHeader); - - String responseString; + JsonArray response; switch (method) { case "put" -> { - responseString = putEntitiesHandler(httpExchange.getRequestBody(), parseRequestAsJson, returnJson); + response = putEntitiesHandler(httpExchange.getRequestBody()); } case "get" -> { - responseString = getEntitiesHandler(returnJson); + response = getEntitiesHandler(); } case "delete" -> { - responseString = deleteEntitiesHandler(httpExchange.getRequestBody(), parseRequestAsJson, returnJson); + response = deleteEntitiesHandler(httpExchange.getRequestBody()); } case "patch" -> { - responseString = patchEntitiesHandler(httpExchange.getRequestBody(), parseRequestAsJson, returnJson); + response = patchEntitiesHandler(httpExchange.getRequestBody()); } default -> { throw new HttpException("Method not allowed. Only PUT, GET, DELETE and PATCH requests are supported.", 405); @@ -107,72 +97,46 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { // Response headers Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - if (returnJson) { - setResponseHeadersContentTypeJson(responseHeaders); - } else { - setResponseHeadersContentTypePlain(responseHeaders); - } - resolveRequest(httpExchange, responseString); + resolveRequest(httpExchange, response.toString()); } /** * @param requestBody request body of entity summon instructions - * @param parseRequestAsJson if true, treat input as JSON - * @param returnJson if true, return result in JSON format * @return summon results */ - private String putEntitiesHandler(InputStream requestBody, boolean parseRequestAsJson, boolean returnJson) { + private JsonArray putEntitiesHandler(InputStream requestBody) { CommandSourceStack cmdSrc = createCommandSource("GDMC-EntitiesHandler", dimension).withPosition(new Vec3(x, y, z)); ServerLevel serverLevel = getServerLevel(dimension); - ArrayList returnValues = new ArrayList<>(); - if (parseRequestAsJson) { - JsonArray entityDescriptionList; - try { - entityDescriptionList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); - } catch (JsonSyntaxException jsonSyntaxException) { - throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); - } + JsonArray returnValues = new JsonArray(); + JsonArray entityDescriptionList; + try { + entityDescriptionList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); + } catch (JsonSyntaxException jsonSyntaxException) { + throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); + } - for (JsonElement entityDescription : entityDescriptionList) { - JsonObject json = entityDescription.getAsJsonObject(); + for (JsonElement entityDescription : entityDescriptionList) { + JsonObject json = entityDescription.getAsJsonObject(); - SummonEntityInstruction summonEntityInstruction; - try { - summonEntityInstruction = new SummonEntityInstruction(json, cmdSrc); - } catch (CommandSyntaxException e) { - returnValues.add(e.getMessage()); - continue; - } - returnValues.add(summonEntityInstruction.summon(serverLevel)); - } - } else { - List inputList = new BufferedReader(new InputStreamReader(requestBody)).lines().toList(); - for (String inputSummonInstruction : inputList) { - SummonEntityInstruction summonEntityInstruction; - try { - summonEntityInstruction = new SummonEntityInstruction(inputSummonInstruction, cmdSrc); - } catch (CommandSyntaxException e) { - returnValues.add(e.getMessage()); - continue; - } - returnValues.add(summonEntityInstruction.summon(serverLevel)); + SummonEntityInstruction summonEntityInstruction; + try { + summonEntityInstruction = new SummonEntityInstruction(json, cmdSrc); + } catch (CommandSyntaxException e) { + returnValues.add(instructionStatus(false, e.getMessage())); + continue; } + returnValues.add(summonEntityInstruction.summon(serverLevel)); } - // Set response as a list of "1" (entity was placed), "0" (entity was not placed) or an exception string if something went wrong. - if (returnJson) { - return new Gson().toJson(returnValues); - } - return String.join("\n", returnValues); + return returnValues; } /** - * @param returnJson if true, return resposne in JSON formatted string * @return list of entity information */ - private String getEntitiesHandler(boolean returnJson) { + private JsonArray getEntitiesHandler() { // Calculate boundaries of area of blocks to gather information on. int xOffset = x + dx; @@ -191,62 +155,44 @@ private String getEntitiesHandler(boolean returnJson) { List entityList = level.getEntities(null, new AABB(xMin, yMin, zMin, xMax, yMax, zMax)); - 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 (Entity entity : entityList) { - String entityId = entity.getEncodeId(); - if (entityId == null) { - continue; - } - JsonObject json = new JsonObject(); - json.addProperty("uuid", entity.getStringUUID()); - if (includeData) { - json.addProperty("data", getEntityDataAsStr(entity)); - } - jsonArray.add(json); - } - return new Gson().toJson(jsonArray); - } - - ArrayList responseList = new ArrayList<>(); + // 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 returnList = new JsonArray(); for (Entity entity : entityList) { String entityId = entity.getEncodeId(); if (entityId == null) { continue; } - responseList.add(entity.getStringUUID() + " " + getEntityDataAsStr(entity)); + JsonObject json = new JsonObject(); + json.addProperty("uuid", entity.getStringUUID()); + if (includeData) { + json.addProperty("data", getEntityDataAsStr(entity)); + } + returnList.add(json); } - return String.join("\n", responseList); + return returnList; } /** * @param requestBody request body of entity removal instructions - * @param parseRequestAsJson if true, treat input as JSON - * @param returnJson if true, return result in JSON format * @return entity removal results */ - private String deleteEntitiesHandler(InputStream requestBody, boolean parseRequestAsJson, boolean returnJson) { + private JsonArray deleteEntitiesHandler(InputStream requestBody) { ServerLevel level = getServerLevel(dimension); List entityUUIDToBeRemoved; - List returnValues = new ArrayList<>(); + JsonArray returnValues = new JsonArray(); - if (parseRequestAsJson) { - JsonArray jsonListUUID; - try { - jsonListUUID = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); - } catch (JsonSyntaxException jsonSyntaxException) { - throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); - } - entityUUIDToBeRemoved = Arrays.asList(new Gson().fromJson(jsonListUUID, String[].class)); - } else { - entityUUIDToBeRemoved = new BufferedReader(new InputStreamReader(requestBody)).lines().toList(); + JsonArray jsonListUUID; + try { + jsonListUUID = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); + } catch (JsonSyntaxException jsonSyntaxException) { + throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); } + entityUUIDToBeRemoved = Arrays.asList(new Gson().fromJson(jsonListUUID, String[].class)); for (String stringUUID : entityUUIDToBeRemoved) { if (stringUUID.length() == 0) { @@ -256,94 +202,62 @@ private String deleteEntitiesHandler(InputStream requestBody, boolean parseReque try { entityToBeRemoved = level.getEntity(UUID.fromString(stringUUID)); } catch (IllegalArgumentException e) { - returnValues.add("0"); + returnValues.add(instructionStatus(false, e.getMessage())); continue; } if (entityToBeRemoved != null) { if (entityToBeRemoved.isRemoved()) { - returnValues.add("0"); + returnValues.add(instructionStatus(false)); } else { entityToBeRemoved.remove(Entity.RemovalReason.DISCARDED); - returnValues.add("1"); + returnValues.add(instructionStatus(true)); } continue; } - returnValues.add("0"); - } - - if (returnJson) { - return new Gson().toJson(returnValues); + returnValues.add(instructionStatus(false)); } - return String.join("\n", returnValues); + return returnValues; } /** * @param requestBody request body of entity patch instructions - * @param parseRequestAsJson if true, treat input as JSON - * @param returnJson if true, return result in JSON-formatted string * @return entity patch status results */ - private String patchEntitiesHandler(InputStream requestBody, boolean parseRequestAsJson, boolean returnJson) { + private JsonArray patchEntitiesHandler(InputStream requestBody) { ServerLevel level = getServerLevel(dimension); - List returnValues = new ArrayList<>(); + JsonArray returnValues = new JsonArray(); - if (parseRequestAsJson) { - JsonArray jsonList; + JsonArray jsonList; + try { + jsonList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); + } catch (JsonSyntaxException jsonSyntaxException) { + throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); + } + for (JsonElement entityDescription : jsonList) { + JsonObject json = entityDescription.getAsJsonObject(); + PatchEntityInstruction patchEntityInstruction; try { - jsonList = JsonParser.parseReader(new InputStreamReader(requestBody)).getAsJsonArray(); - } catch (JsonSyntaxException jsonSyntaxException) { - throw new HttpException("Malformed JSON: " + jsonSyntaxException.getMessage(), 400); - } - for (JsonElement entityDescription : jsonList) { - JsonObject json = entityDescription.getAsJsonObject(); - PatchEntityInstruction patchEntityInstruction; - try { - patchEntityInstruction = new PatchEntityInstruction(json); - } catch (IllegalArgumentException | CommandSyntaxException | UnsupportedOperationException e) { - returnValues.add(e.getMessage()); - continue; - } - try { - if (patchEntityInstruction.applyPatch(level)) { - returnValues.add("1"); - continue; - } - } catch (ReportedException e) { - returnValues.add(e.getMessage()); - continue; - } - returnValues.add("0"); + patchEntityInstruction = new PatchEntityInstruction(json); + } catch (IllegalArgumentException | CommandSyntaxException | UnsupportedOperationException e) { + returnValues.add(instructionStatus(false, e.getMessage())); + continue; } - } else { - List textList = new BufferedReader(new InputStreamReader(requestBody)).lines().toList(); - for (String entityDescription : textList) { - PatchEntityInstruction patchEntityInstruction; - try { - patchEntityInstruction = new PatchEntityInstruction(entityDescription); - } catch (IllegalArgumentException | CommandSyntaxException e) { - returnValues.add(e.getMessage()); - continue; - } - try { - if (patchEntityInstruction.applyPatch(level)) { - returnValues.add("1"); - continue; - } - } catch (ReportedException e) { - returnValues.add(e.getMessage()); + try { + if (patchEntityInstruction.applyPatch(level)) { + returnValues.add(instructionStatus(true)); continue; } - returnValues.add("0"); + } catch (ReportedException e) { + returnValues.add(instructionStatus(false, e.getMessage())); + continue; } + returnValues.add(instructionStatus(false)); } - if (returnJson) { - return new Gson().toJson(returnValues); - } - return String.join("\n", returnValues); + return returnValues; } private String getEntityDataAsStr(Entity entity) { @@ -374,10 +288,6 @@ private final static class SummonEntityInstruction { parse(positionArgumentString + " " + entityIDString + " " + entityDataString, commandSourceStack); } - SummonEntityInstruction(String inputData, CommandSourceStack commandSourceStack) throws CommandSyntaxException { - parse(inputData, commandSourceStack); - } - private void parse(String inputData, CommandSourceStack commandSourceStack) throws CommandSyntaxException { StringReader sr = new StringReader(inputData); @@ -398,9 +308,9 @@ private void parse(String inputData, CommandSourceStack commandSourceStack) thro } } - public String summon(ServerLevel level) { + public JsonObject summon(ServerLevel level) { if (!Level.isInSpawnableBounds(new BlockPos(entityPosition))) { - return "Position is not in spawnable bounds"; + return instructionStatus(false, "Position is not in spawnable bounds"); } entityData.putString("id", entityResourceLocation.toString()); @@ -410,16 +320,16 @@ public String summon(ServerLevel level) { return _entity; }); if (entity == null) { - return "Entity could not be spawned"; + return instructionStatus(false, "Entity could not be spawned"); } entity.checkDespawn(); if (entity.isRemoved()) { - return "Entity was removed right after spawn for reason: " + entity.getRemovalReason(); + return instructionStatus(false, "Entity was removed right after spawn for reason: " + entity.getRemovalReason()); } if (!level.tryAddFreshEntityWithPassengers(entity)) { - return "Entity with this UUID already exists"; + return instructionStatus(false, "Entity with this UUID already exists"); } - return entity.getStringUUID(); + return instructionStatus(true, entity.getStringUUID()); } } @@ -436,12 +346,6 @@ private final static class PatchEntityInstruction { patchData = TagParser.parseTag(inputData.get("data").getAsString()); } - PatchEntityInstruction(String inputData) throws IllegalArgumentException, CommandSyntaxException { - StringReader sr = new StringReader(inputData); - uuid = UUID.fromString(sr.readStringUntil(' ')); - patchData = TagParser.parseTag(sr.getRemaining()); - } - public boolean applyPatch(ServerLevel level) throws ReportedException { if (uuid == null) { return false; diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java index 634d277..78c1f64 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/HandlerBase.java @@ -1,5 +1,6 @@ package com.gdmc.httpinterfacemod.handlers; +import com.google.gson.JsonObject; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -53,10 +54,13 @@ public void handle(HttpExchange httpExchange) throws IOException { try { internalHandle(httpExchange); } catch (HttpException e) { - String responseString = e.message; - byte[] responseBytes = responseString.getBytes(StandardCharsets.UTF_8); + JsonObject json = new JsonObject(); + json.addProperty("status", e.statusCode); + json.addProperty("message", e.message); + + byte[] responseBytes = json.toString().getBytes(StandardCharsets.UTF_8); Headers headers = httpExchange.getResponseHeaders(); - headers.set("Content-Type", "text/plain; charset=UTF-8"); + setResponseHeadersContentTypeJson(headers); httpExchange.sendResponseHeaders(e.statusCode, responseBytes.length); OutputStream outputStream = httpExchange.getResponseBody(); @@ -66,19 +70,21 @@ public void handle(HttpExchange httpExchange) throws IOException { LOGGER.log(Level.ERROR, e.message); } catch (Exception e) { // create a response string with stacktrace + int statusCode = 500; String stackTrace = ExceptionUtils.getStackTrace(e); - - String responseString = String.format("Internal server error: %s\n%s", e, stackTrace); - byte[] responseBytes = responseString.getBytes(StandardCharsets.UTF_8); + JsonObject json = new JsonObject(); + json.addProperty("status", statusCode); + json.addProperty("message", stackTrace); + byte[] responseBytes = json.toString().getBytes(StandardCharsets.UTF_8); Headers headers = httpExchange.getResponseHeaders(); - headers.set("Content-Type", "text/plain; charset=UTF-8"); + setResponseHeadersContentTypeJson(headers); httpExchange.sendResponseHeaders(500, responseBytes.length); OutputStream outputStream = httpExchange.getResponseBody(); outputStream.write(responseBytes); outputStream.close(); - LOGGER.log(Level.ERROR, responseString); + LOGGER.log(Level.ERROR, stackTrace); throw e; } } @@ -144,6 +150,7 @@ protected static boolean hasJsonTypeInHeader(String header) { protected static void setDefaultResponseHeaders(Headers headers) { headers.set("Access-Control-Allow-Origin", "*"); headers.set("Content-Disposition", "inline"); + setResponseHeadersContentTypeJson(headers); } /** @@ -219,6 +226,18 @@ protected static Map parseQueryString(String qs) { return result; } + protected static JsonObject instructionStatus(boolean isSuccess) { + return instructionStatus(isSuccess, null); + } + protected static JsonObject instructionStatus(boolean isSuccess, String message) { + JsonObject json = new JsonObject(); + json.addProperty("status", isSuccess ? 1 : 0); + if (message != null) { + json.addProperty("message", message); + } + return json; + } + /** * Helper to create a {@link CommandSourceStack}, which serves as the source to dispatch * commands from (See {@link CommandHandler}) or as a point of origin to place blocks diff --git a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java index 711bd8f..01db5eb 100644 --- a/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java +++ b/src/main/java/com/gdmc/httpinterfacemod/handlers/StructureHandler.java @@ -1,6 +1,6 @@ package com.gdmc.httpinterfacemod.handlers; -import com.google.gson.Gson; +import com.google.gson.JsonObject; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import net.minecraft.core.BlockPos; @@ -107,7 +107,6 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { Headers requestHeaders = httpExchange.getRequestHeaders(); String acceptHeader = getHeader(requestHeaders, "Accept", "*/*"); boolean returnPlainText = acceptHeader.equals("text/plain"); - boolean returnJson = hasJsonTypeInHeader(acceptHeader); switch (httpExchange.getRequestMethod().toLowerCase()) { case "post" -> { @@ -117,7 +116,7 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { // stored in this compressed format. String contentEncodingHeader = getHeader(requestHeaders, "Content-Encoding", "*"); boolean inputShouldBeCompressed = contentEncodingHeader.equals("gzip"); - postStructureHandler(httpExchange, inputShouldBeCompressed, returnJson); + postStructureHandler(httpExchange, inputShouldBeCompressed); } case "get" -> { // If "Accept-Encoding" header is set to "gzip" and the client expects a binary format, @@ -130,8 +129,8 @@ protected void internalHandle(HttpExchange httpExchange) throws IOException { } } - private void postStructureHandler(HttpExchange httpExchange, boolean parseRequestAsGzip, boolean returnJson) throws IOException { - String responseString; + private void postStructureHandler(HttpExchange httpExchange, boolean parseRequestAsGzip) throws IOException { + JsonObject responseValue; CompoundTag structureCompound; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -205,9 +204,9 @@ private void postStructureHandler(HttpExchange httpExchange, boolean parseReques } } } - responseString = "1"; + responseValue = instructionStatus(true); } else { - responseString = "0"; + responseValue = instructionStatus(false); } } catch (Exception exception) { throw new HttpException("Could not place structure: " + exception.getMessage(), 400); @@ -215,14 +214,8 @@ private void postStructureHandler(HttpExchange httpExchange, boolean parseReques Headers responseHeaders = httpExchange.getResponseHeaders(); setDefaultResponseHeaders(responseHeaders); - if (returnJson) { - responseString = new Gson().toJson(responseString); - setResponseHeadersContentTypeJson(responseHeaders); - } else { - setResponseHeadersContentTypePlain(responseHeaders); - } - resolveRequest(httpExchange, responseString); + resolveRequest(httpExchange, responseValue.toString()); } private void getStructureHandler(HttpExchange httpExchange, boolean returnPlainText, boolean returnCompressed) throws IOException {