diff --git a/README.md b/README.md index 7de1fcf..36114ad 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,3 @@ git clone git@github.com:andrewlalis/MaterialsExtractor.git cd MaterialsExtractor ./build.sh ``` - -Usage information: -``` -java -jar materials-extractor.jar [paste.ee token] - Where - First argument (required) is the path to the .nbt schematic file to read. - Second argument (optional) is an api token to https://paste.ee, to - automatically upload item-list for usage. -``` diff --git a/build.sh b/build.sh index 93e1a2f..7c6da81 100755 --- a/build.sh +++ b/build.sh @@ -4,4 +4,4 @@ cp target/materials-extractor-*-jar-with-dependencies.jar ./materials-extractor.jar echo "Built project as materials-extractor.jar" -echo "Run: java -jar materials-extractor.jar [paste.ee token]" +echo "Run: java -jar materials-extractor.jar " diff --git a/pom.xml b/pom.xml index 859c66f..a1bbcdd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ nl.andrewlalis materials-extractor - 0.0.2 + 1.0.0 17 17 diff --git a/src/main/java/nl/andrewlalis/materials_extractor/MaterialsExtractor.java b/src/main/java/nl/andrewlalis/materials_extractor/MaterialsExtractor.java index 1647794..6274433 100644 --- a/src/main/java/nl/andrewlalis/materials_extractor/MaterialsExtractor.java +++ b/src/main/java/nl/andrewlalis/materials_extractor/MaterialsExtractor.java @@ -1,6 +1,8 @@ package nl.andrewlalis.materials_extractor; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import net.querz.nbt.io.NBTUtil; import net.querz.nbt.io.NamedTag; @@ -8,49 +10,95 @@ import net.querz.nbt.tag.CompoundTag; import net.querz.nbt.tag.ListTag; import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public class MaterialsExtractor { - private static final Map CONVERSIONS = new HashMap<>(); - static { - CONVERSIONS.put("minecraft:redstone_wall_torch", "minecraft:redstone_torch"); - CONVERSIONS.put("minecraft:piston_head", null); - CONVERSIONS.put("minecraft:redstone_wire", "minecraft:redstone"); - } - private static final ObjectMapper MAPPER = new ObjectMapper(); public static void main(String[] args) throws Exception { if (args.length < 1) { - System.out.println("Missing required NBT file as 1st argument to this program."); + System.out.println("Missing at least one NBT file as an argument to the program."); System.exit(1); } - Path nbtFilePath = Path.of(args[0]); - if (Files.notExists(nbtFilePath) || !Files.isReadable(nbtFilePath) || !nbtFilePath.getFileName().toString().endsWith(".nbt")) { - System.out.println("Invalid or missing file: " + args[0]); - System.exit(1); + for (String arg : args) { + if (!validateInputFile(arg)) { + System.out.println("Invalid or missing file: " + arg); + System.exit(1); + } } - final var counts = getItemList(nbtFilePath); + var conversions = parseConversions(); + ArrayNode array = MAPPER.createArrayNode(); + for (String arg : args) { + array.add(extractFromFile(arg, conversions)); + } + ObjectMapper outputMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + String json = outputMapper.writeValueAsString(array); + System.out.println(json); + } - ObjectNode obj = MAPPER.createObjectNode(); - counts.keySet().stream().sorted().forEachOrdered(key -> obj.put(key, counts.get(key))); - String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj); - if (args.length >= 2) { - // Use 2nd arg as api key for paste.ee. - upload(json, args[1]); - } else { - System.out.println("Copy and paste the following JSON to a pastebin service, and copy the \"raw\" download URL when running the item-extractor program on your in-game PC:"); - System.out.println(json); + private static boolean validateInputFile(String file) { + Path filePath = Path.of(file); + return Files.exists(filePath) && + Files.isReadable(filePath) && + filePath.getFileName().toString().toLowerCase().endsWith(".nbt"); + } + + private static Map parseConversions() throws IOException { + try (var in = MaterialsExtractor.class.getResourceAsStream("/conversions.json")) { + if (in == null) throw new IOException("Couldn't find resource."); + ObjectNode obj = MAPPER.readValue(in, ObjectNode.class); + Map conversions = new HashMap<>(); + for (var entry : obj.properties()) { + if (entry.getValue().isNull()) { + conversions.put(entry.getKey(), null); + } else { + conversions.put(entry.getKey(), entry.getValue().asText()); + } + } + return Collections.unmodifiableMap(conversions); } } - private static Map getItemList(Path filePath) throws IOException { + private static ObjectNode extractFromFile(String file, Map conversions) throws IOException { + var filePath = Path.of(file); + var counts = getItemList(filePath, conversions); + var obj = countsToJson(counts); + obj.put("__NAME__", filePath.getFileName().toString()); + obj.put("__TIMESTAMP__", LocalDateTime.now(ZoneOffset.UTC).toString()); + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] hash = md.digest(Files.readAllBytes(filePath)); + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) sb.append(String.format("%02x", b)); + obj.put("__SHA1_HASH__", sb.toString()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return obj; + } + + /** + * Gets a mapping of items to their required count, for a Create-mod + * NBT schematic file. The NBT is structured such that it contains a + * palette list of compound tags, and a blocks + * list of compound tags. We first parse an array of all item names from the + * palette, then look through the blocks to count up the instances of each. + * @param filePath The file path to read. + * @param conversions A mapping for item names to convert to others (or null) + * in order to avoid weird minecraft items like piston + * heads. + * @return A mapping of item names to their count. + * @throws IOException If a read error occurs. + */ + private static Map getItemList(Path filePath, Map conversions) throws IOException { NamedTag root = NBTUtil.read(filePath.toFile()); CompoundTag compoundTag = (CompoundTag) root.getTag(); ListTag paletteEntries = compoundTag.getListTag("palette").asCompoundTagList(); @@ -64,7 +112,7 @@ public class MaterialsExtractor { Map counts = new HashMap<>(); for (var entry : blockEntries) { String name = palette[entry.getInt("state")]; - String converted = CONVERSIONS.getOrDefault(name, name); + String converted = conversions.getOrDefault(name, name); if (converted != null) { counts.put(converted, 1 + counts.computeIfAbsent(converted, s -> 0)); } @@ -72,31 +120,9 @@ public class MaterialsExtractor { return counts; } - private static void upload(String content, String apiToken) throws Exception { - HttpClient httpClient = HttpClient.newHttpClient(); - - ObjectNode body = MAPPER.createObjectNode(); - body.put("encrypted", false); - body.put("description", "Schematic item-list"); - ObjectNode section = MAPPER.createObjectNode(); - section.put("name", "item-list.json"); - section.put("syntax", "json"); - section.put("contents", content); - body.set("sections", MAPPER.createArrayNode().add(section)); - - - HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.paste.ee/v1/pastes")) - .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(body))) - .header("X-Auth-Token", apiToken) - .header("Content-Type", "application/json") - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() >= 400) { - System.err.println("Error: " + response.statusCode()); - return; - } - ObjectNode responseBody = MAPPER.readValue(response.body(), ObjectNode.class); - String pasteId = responseBody.get("id").asText(); - System.out.println("https://paste.ee/d/" + pasteId); + private static ObjectNode countsToJson(Map counts) { + ObjectNode obj = MAPPER.createObjectNode(); + counts.keySet().stream().sorted().forEachOrdered(key -> obj.put(key, counts.get(key))); + return obj; } } diff --git a/src/main/resources/conversions.json b/src/main/resources/conversions.json new file mode 100644 index 0000000..77a6e21 --- /dev/null +++ b/src/main/resources/conversions.json @@ -0,0 +1,5 @@ +{ + "minecraft:redstone_wall_torch": "minecraft:redstone_torch", + "minecraft:piston_head": null, + "minecraft:redstone_wire": "minecraft:redstone" +}