From dc6bc7adc0f2efeb65c002f667847c7237468b76 Mon Sep 17 00:00:00 2001 From: Meeples10 Date: Sun, 5 Dec 2021 15:32:45 -0500 Subject: [PATCH] Add support for Minecraft 1.18 --- README.md | 6 +- pom.xml | 2 +- .../mcresourceanalyzer/RegionAnalyzer.java | 13 +- .../RegionAnalyzerAnvil118.java | 185 ++++++++++++++++++ 4 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzerAnvil118.java diff --git a/README.md b/README.md index 184cce5..05701c7 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Several command line arguments may be used to modify the behavior of the program - `table`: Generates a simple HTML table with the collected data. - `version-select`: Use this argument if you want to analyze a world that was not generated with the latest version of Minecraft. Shows a popup on launch that allows the version in which the region files were generated to be selected. Selecting a version that does not match the version in which the regions were generated may result in unexpected behavior. Alternatively, to explicitly specify a version from the command line and skip the popup, use the argument `version-select=` where `` is one of the following: + - `ANVIL_118` for 1.18 - `ANVIL_2021` for 1.16 to 1.17 - `ANVIL_2018` for 1.13 to 1.15 - `ANVIL_2012` for 1.2 to 1.12 @@ -37,7 +38,7 @@ Alternatively, to explicitly specify a version from the command line and skip th ### Version compatibility -MCResourceAnalyzer 1.1.1 can analyze worlds generated with any version of Minecraft: Java Edition between Indev 0.31 20100122 and 1.17. +MCResourceAnalyzer 1.1.4 can analyze worlds generated with any version of Minecraft: Java Edition between Indev 0.31 20100122 and 1.18. Note that Indev worlds with the `Long` and `Deep` world shapes are not supported. @@ -46,6 +47,3 @@ Note that Indev worlds with the `Long` and `Deep` world shapes are not supported Contributions to this project are more than welcome. If you create your own fork, my only request is that you include the following URL somewhere in your fork: https://github.com/Meeples10/MCResourceAnalyzer If you notice that the program fails to analyze a particular world, please [create an issue](https://github.com/Meeples10/MCResourceAnalyzer/issues/new?title=Error%20when%20analyzing%20world&body=Minecraft%20version:%20%0A%0AProgram%20output:%0A%60%60%60%0A[paste%20output%20here]%0A%60%60%60%0A%0AOther%20details:%20%0A%0A%3C%21--%20If%20possible,%20please%20attach%20the%20region%20file%20that%20caused%20the%20program%20to%20fail%20--%3E) with the version of Minecraft with which the world was generated, the output of the program, and, if possible, the region file that caused the program to fail. - - -[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/donate?business=ZXKMCY4HP34BG&no_recurring=0&item_name=Developing+Software¤cy_code=USD) diff --git a/pom.xml b/pom.xml index 46ba49c..44713cc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.github.meeples10.mcresourceanalyzer mc-resource-analyzer - 1.1.3 + 1.1.4 jar diff --git a/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzer.java b/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzer.java index 40d0f50..f62a903 100644 --- a/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzer.java +++ b/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzer.java @@ -106,12 +106,13 @@ int getMaximumY() { } public enum Version { - ANVIL_2021("Anvil (1.16 to 1.17)", RegionAnalyzerAnvil2021.class, true), ANVIL_2018("Anvil (1.13 to 1.15)", - RegionAnalyzerAnvil2018.class, - true), ANVIL_2012("Anvil (1.2 to 1.12)", RegionAnalyzerAnvil2012.class, true), MCREGION( - "McRegion (Beta 1.3 to 1.1)", RegionAnalyzerMCRegion.class, - true), ALPHA("Alpha (Infdev 20100327 to Beta 1.2)", RegionAnalyzerAlpha.class, true), INDEV( - "Indev (Indev 0.31 20100122 to Infdev 20100325)", RegionAnalyzerIndev.class, false); + ANVIL_118("Anvil (1.18)", RegionAnalyzerAnvil118.class, true), ANVIL_2021("Anvil (1.16 to 1.17)", + RegionAnalyzerAnvil2021.class, true), ANVIL_2018("Anvil (1.13 to 1.15)", RegionAnalyzerAnvil2018.class, + true), ANVIL_2012("Anvil (1.2 to 1.12)", RegionAnalyzerAnvil2012.class, true), MCREGION( + "McRegion (Beta 1.3 to 1.1)", RegionAnalyzerMCRegion.class, + true), ALPHA("Alpha (Infdev 20100327 to Beta 1.2)", RegionAnalyzerAlpha.class, + true), INDEV("Indev (Indev 0.31 20100122 to Infdev 20100325)", + RegionAnalyzerIndev.class, false); private final String versionName; private final Class analyzerClass; diff --git a/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzerAnvil118.java b/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzerAnvil118.java new file mode 100644 index 0000000..1f51cbc --- /dev/null +++ b/src/main/java/io/github/meeples10/mcresourceanalyzer/RegionAnalyzerAnvil118.java @@ -0,0 +1,185 @@ +package io.github.meeples10.mcresourceanalyzer; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagLongArray; + +public class RegionAnalyzerAnvil118 extends RegionAnalyzer { + + @Override + public void analyze(File regionDir) { + if(!regionDir.exists()) { + System.out.println("Error: No region directory found at " + regionDir.getAbsolutePath()); + System.exit(1); + } + int totalRegions = regionDir.listFiles().length; + if(totalRegions == 0) { + System.out.println("Error: Region directory is empty"); + System.exit(1); + } + System.out.println(totalRegions + " regions found"); + int rnum = 1; + for(File f : regionDir.listFiles()) { + long startTime = System.currentTimeMillis(); + String name = Main.formatRegionName(regionDir, f); + RegionFile r = new RegionFile(f); + System.out.print("Scanning region " + name + " [" + rnum + "/" + totalRegions + "] (modified " + + Main.DATE_FORMAT.format(new Date(r.lastModified())) + ")... "); + for(int x = 0; x < 32; x++) { + for(int z = 0; z < 32; z++) { + if(r.hasChunk(x, z)) { + try { + processRegion(r, name, x, z); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + } + System.out.println( + "Done (" + String.format("%.2f", (double) (System.currentTimeMillis() - startTime) / 1000) + "s)"); + rnum++; + } + long duration = System.currentTimeMillis() - getStartTime(); + System.out.println(("Completed analysis in " + Main.millisToHMS(duration) + " (" + chunkCount + " chunks)")); + long totalBlocks = 0L; + for(String key : blockCounter.keySet()) { + totalBlocks += blockCounter.get(key); + } + System.out.println("--------------------------------\n" + blockCounter.size() + " unique blocks\n" + totalBlocks + + " blocks total\n--------------------------------"); + + System.out.print("Sorting data... "); + heightCounter = heightCounter.entrySet().stream().sorted(Map.Entry.comparingByKey(new Comparator() { + @Override + public int compare(String arg0, String arg1) { + return Long.compare(blockCounter.get(arg1), blockCounter.get(arg0)); + } + })).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + System.out.println("Done"); + + double totalExcludingAir = (double) (totalBlocks + - (blockCounter.containsKey("minecraft:air") ? blockCounter.get("minecraft:air") : 0) + - (blockCounter.containsKey("minecraft:cave_air") ? blockCounter.get("minecraft:cave_air") : 0)); + System.out.print("Generating CSV... "); + String data = ""; + if(Main.saveStatistics) { + data += "chunk-count=" + chunkCount + ",unique-blocks=" + blockCounter.size() + ",total-blocks=" + + totalBlocks + ",duration-millis=" + duration + ",duration-readable=" + Main.millisToHMS(duration) + + "\n"; + } + data += "id,"; + int minY = getMinimumY(); + int maxY = getMaximumY(); + for(int i = minY; i <= maxY; i++) { + data += i + ","; + } + data += "total,percent_of_total,percent_excluding_air\n"; + int digits = String.valueOf(blockCounter.size()).length(); + String completionFormat = "[%0" + digits + "d/%0" + digits + "d]"; + int keyIndex = 0; + for(String key : heightCounter.keySet()) { + keyIndex += 1; + System.out.print("\rGenerating CSV... " + String.format(completionFormat, keyIndex, blockCounter.size())); + data += key + ","; + for(int i = minY; i <= maxY; i++) { + if(!heightCounter.get(key).containsKey(i)) { + data += "0,"; + } else { + data += heightCounter.get(key).get(i) + ","; + } + } + data += blockCounter.get(key) + "," + + Main.DECIMAL_FORMAT.format(((double) blockCounter.get(key) / (double) totalBlocks) * 100.0d); + if(key.equals("minecraft:air") || key.equals("minecraft:cave_air")) { + data += ",N/A"; + } else { + data += "," + Main.DECIMAL_FORMAT.format(((double) blockCounter.get(key) / totalExcludingAir) * 100.0d); + } + data += "\n"; + } + try { + File out = new File(Main.getOutputPrefix() + ".csv"); + Main.writeStringToFile(out, data); + System.out.println("\nData written to " + out.getAbsolutePath()); + } catch(IOException e) { + e.printStackTrace(); + System.exit(1); + } + if(Main.generateTable) { + try { + File out = new File(Main.getOutputPrefix() + "_table.html"); + Main.writeStringToFile(out, generateTable((double) totalBlocks, totalExcludingAir)); + System.out.println("\nTable written to " + out.getAbsolutePath()); + } catch(IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + } + + private void processRegion(RegionFile r, String name, int x, int z) throws Exception { + DataInputStream chunkDataInputStream = r.getChunkDataInputStream(x, z); + if(chunkDataInputStream == null) { + // Skip malformed chunks + return; + } + NBTTagList sections = CompressedStreamTools.read(r.getChunkDataInputStream(x, z)).getTagList("sections", 10); + analyzeChunk(sections); + chunkCount++; + } + + private void analyzeChunk(NBTTagList sections) { + int i = 0; + for(; i < sections.tagCount(); i++) { + NBTTagCompound tag = sections.getCompoundTagAt(i); + + NBTTagLongArray blockStatesTag = ((NBTTagLongArray) tag.getCompoundTag("block_states").getTag("data")); + long[] blockStates = blockStatesTag == null ? new long[0] : blockStatesTag.get(); + NBTTagList palette = tag.getCompoundTag("block_states").getTagList("palette", 10); + if(palette.hasNoTags()) continue; + int bitLength = Main.bitLength(palette.tagCount() - 1); + bitLength = bitLength < 4 ? 4 : bitLength; + int[] blocks = Main.unstream(bitLength, 64, true, blockStates); + if(blocks.length == 0) continue; // skip empty sections + int sectionY = tag.getByte("Y"); + + for(int y = 0; y < 16; y++) { + for(int x = 0; x < 16; x++) { + for(int z = 0; z < 16; z++) { + int actualY = sectionY * 16 + y; + int blockIndex = y * 16 * 16 + z * 16 + x; + String block = palette.getStringTagAt(blocks[blockIndex]); + int startIndex = block.indexOf("Name:\""); + String blockName = block.substring(startIndex + 6, block.indexOf('"', startIndex + 6)); + if(blockCounter.containsKey(blockName)) { + blockCounter.put(blockName, blockCounter.get(blockName) + 1L); + } else { + blockCounter.put(blockName, 1L); + } + if(!heightCounter.containsKey(blockName)) { + heightCounter.put(blockName, new HashMap()); + } + if(heightCounter.get(blockName).containsKey(actualY)) { + heightCounter.get(blockName).put(actualY, heightCounter.get(blockName).get(actualY) + 1L); + } else { + heightCounter.get(blockName).put(actualY, 1L); + } + } + } + } + } + airHack(i, "minecraft:air"); + } +}