diff --git a/src/se/icus/mag/pmanvil/PMAnvilConverter.java b/src/se/icus/mag/pmanvil/PMAnvilConverter.java
new file mode 100755
index 0000000..46ee46f
--- /dev/null
+++ b/src/se/icus/mag/pmanvil/PMAnvilConverter.java
@@ -0,0 +1,84 @@
+package se.icus.mag.pmanvil;
+
+/**
+ * Copyright Mojang AB.
+ *
+ * Don't do evil.
+ *
+ * Adaptations for PMAnvil conversions by magicus 2018.
+ */
+
+
+import net.minecraft.world.level.storage.ProgressListener;
+
+import java.io.File;
+
+public class PMAnvilConverter {
+
+ public static void main(String[] args) {
+
+ if (args.length != 2) {
+ printUsageAndExit();
+ }
+
+ File baseFolder;
+ try {
+ baseFolder = new File(args[0]);
+ if (!baseFolder.exists()) {
+ throw new RuntimeException(args[0] + " doesn't exist");
+ } else if (!baseFolder.isDirectory()) {
+ throw new RuntimeException(args[0] + " is not a folder");
+ }
+ } catch (Exception e) {
+ System.err.println("Base folder problem: " + e.getMessage());
+ System.out.println("");
+ printUsageAndExit();
+ return;
+ }
+
+ PMAnvilLevelStorageSource storage = new PMAnvilLevelStorageSource(baseFolder);
+ if (!storage.isConvertible(args[1])) {
+ System.err.println("World called " + args[1] + " is not convertible to the Anvil format");
+ System.out.println("");
+ printUsageAndExit();
+ return;
+ }
+
+ System.out.println("Converting map!");
+ storage.convertLevel(args[1], new ProgressListener() {
+ private long timeStamp = System.currentTimeMillis();
+
+ public void progressStartNoAbort(String string) {
+ }
+
+ public void progressStart(String string) {
+ }
+
+ public void progressStagePercentage(int i) {
+ if ((System.currentTimeMillis() - timeStamp) >= 1000L) {
+ timeStamp = System.currentTimeMillis();
+ System.out.println("Converting... " + i + "%");
+ }
+ }
+
+ public void progressStage(String string) {
+ }
+ });
+ System.out.println("Done!");
+ System.out.println("You can now delete all *.mcapm files in the converted world.");
+ }
+
+ private static void printUsageAndExit() {
+ System.out.println("Map converter for Minecraft, from format \"PMAnvil\" to \"Anvil\". (c) Mojang AB 2012, magicus 2018");
+ System.out.println("");
+ System.out.println("Usage:");
+ System.out.println("\tjava -jar pmanvil-converter.jar ");
+ System.out.println("Where:");
+ System.out.println("\t\tThe full path to the folder containing Minecraft world folders");
+ System.out.println("\t\tThe folder name of the Minecraft world to be converted");
+ System.out.println("Example:");
+ System.out.println("\tjava -jar pmanvil-converter.jar /home/jeb_/minecraft world");
+ System.exit(1);
+ }
+
+}
diff --git a/src/se/icus/mag/pmanvil/PMAnvilLevelStorageSource.java b/src/se/icus/mag/pmanvil/PMAnvilLevelStorageSource.java
new file mode 100755
index 0000000..b0731e4
--- /dev/null
+++ b/src/se/icus/mag/pmanvil/PMAnvilLevelStorageSource.java
@@ -0,0 +1,278 @@
+package se.icus.mag.pmanvil;
+
+/**
+ * Copyright Mojang AB.
+ *
+ * Don't do evil.
+ *
+ * Adaptations for PMAnvil conversions by magicus 2018.
+ */
+
+import java.io.*;
+import java.util.ArrayList;
+
+import net.minecraft.world.level.chunk.storage.*;
+
+import com.mojang.nbt.*;
+import net.minecraft.world.level.storage.LevelStorage;
+import net.minecraft.world.level.storage.ProgressListener;
+
+public class PMAnvilLevelStorageSource {
+
+ public static final String PM_ANVIL_EXTENSION = ".mcapm";
+
+ private File baseDir;
+
+ public PMAnvilLevelStorageSource(File dir) {
+ baseDir = dir;
+ }
+
+ public boolean isConvertible(String levelId) {
+
+ // Pocketmine does not set level data version properly, so ignore it
+ // Instead, check if there is mcapm files present
+ File baseFolder = new File(baseDir, levelId);
+ File regionFolder = new File(baseFolder, "region");
+ File[] list = regionFolder.listFiles(new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name.endsWith(PM_ANVIL_EXTENSION);
+ }
+ });
+
+ if (list == null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private CompoundTag getDataTagFor(String levelId) {
+ File dir = new File(baseDir, levelId);
+ if (!dir.exists()) return null;
+
+ File dataFile = new File(dir, "level.dat");
+ if (dataFile.exists()) {
+ try {
+ CompoundTag root = NbtIo.readCompressed(new FileInputStream(dataFile));
+ CompoundTag tag = root.getCompound("Data");
+ return tag;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ dataFile = new File(dir, "level.dat_old");
+ if (dataFile.exists()) {
+ try {
+ CompoundTag root = NbtIo.readCompressed(new FileInputStream(dataFile));
+ CompoundTag tag = root.getCompound("Data");
+ return tag;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ public boolean convertLevel(String levelId, ProgressListener progress) {
+
+ progress.progressStagePercentage(0);
+
+ ArrayList normalRegions = new ArrayList();
+ ArrayList netherRegions = new ArrayList();
+ ArrayList enderRegions = new ArrayList();
+//
+ File baseFolder = new File(baseDir, levelId);
+ File netherFolder = new File(baseFolder, LevelStorage.NETHER_FOLDER);
+ File enderFolder = new File(baseFolder, LevelStorage.ENDER_FOLDER);
+
+ System.out.println("Scanning folders...");
+
+ // find normal world
+ addRegionFiles(baseFolder, normalRegions);
+
+ // find hell world
+ if (netherFolder.exists()) {
+ addRegionFiles(netherFolder, netherRegions);
+ }
+ if (enderFolder.exists()) {
+ addRegionFiles(enderFolder, enderRegions);
+ }
+
+ int totalCount = normalRegions.size() + netherRegions.size() + enderRegions.size();
+ System.out.println("Total conversion count is " + totalCount);
+
+ // convert normal world
+ convertRegions(new File(baseFolder, "region"), normalRegions, 0, totalCount, progress);
+ // convert hell world
+ convertRegions(new File(netherFolder, "region"), netherRegions, normalRegions.size(), totalCount, progress);
+ // convert end world
+ convertRegions(new File(enderFolder, "region"), enderRegions, normalRegions.size() + netherRegions.size(), totalCount, progress);
+
+
+ return true;
+ }
+
+ private void convertRegions(File baseFolder, ArrayList regionFiles, int currentCount, int totalCount, ProgressListener progress) {
+
+ for (File regionFile : regionFiles) {
+ convertRegion(baseFolder, regionFile, currentCount, totalCount, progress);
+
+ currentCount++;
+ int percent = (int) Math.round(100.0d * (double) currentCount / (double) totalCount);
+ progress.progressStagePercentage(percent);
+ }
+
+ }
+
+ private void convertRegion(File baseFolder, File regionFile, int currentCount, int totalCount, ProgressListener progress) {
+
+ try {
+ String name = regionFile.getName();
+
+ RegionFile regionSource = new RegionFile(regionFile);
+ RegionFile regionDest = new RegionFile(new File(baseFolder, name.substring(0, name.length() - PM_ANVIL_EXTENSION.length()) + RegionFile.ANVIL_EXTENSION));
+
+ for (int x = 0; x < 32; x++) {
+ for (int z = 0; z < 32; z++) {
+ if (regionSource.hasChunk(x, z) && !regionDest.hasChunk(x, z)) {
+ DataInputStream regionChunkInputStream = regionSource.getChunkDataInputStream(x, z);
+ if (regionChunkInputStream == null) {
+ System.out.println("Failed to fetch input stream");
+ continue;
+ }
+ CompoundTag chunkData = NbtIo.read(regionChunkInputStream);
+ regionChunkInputStream.close();
+
+ CompoundTag compound = chunkData.getCompound("Level");
+ {
+ CompoundTag tag = new CompoundTag();
+ CompoundTag levelData = new CompoundTag();
+ tag.put("Level", levelData);
+ convertFromPmAnvilFormat(compound, levelData);
+
+ DataOutputStream chunkDataOutputStream = regionDest.getChunkDataOutputStream(x, z);
+ NbtIo.write(tag, chunkDataOutputStream);
+ chunkDataOutputStream.close();
+ }
+ }
+ }
+ int basePercent = (int) Math.round(100.0d * (double) (currentCount * 1024) / (double) (totalCount * 1024));
+ int newPercent = (int) Math.round(100.0d * (double) ((x + 1) * 32 + currentCount * 1024) / (double) (totalCount * 1024));
+ if (newPercent > basePercent) {
+ progress.progressStagePercentage(newPercent);
+ }
+ }
+
+ regionSource.close();
+ regionDest.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void addRegionFiles(File baseFolder, ArrayList regionFiles) {
+
+ File regionFolder = new File(baseFolder, "region");
+ File[] list = regionFolder.listFiles(new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name.endsWith(PM_ANVIL_EXTENSION);
+ }
+ });
+
+ if (list != null) {
+ for (File file : list) {
+ regionFiles.add(file);
+ }
+ }
+ }
+
+ private void convertFromPmAnvilFormat(CompoundTag inTag, CompoundTag outTag) {
+
+ outTag.putInt("xPos", inTag.getInt("xPos"));
+ outTag.putInt("zPos", inTag.getInt("zPos"));
+ outTag.putLong("LastUpdate", inTag.getLong("LastUpdate"));
+ outTag.putIntArray("HeightMap", inTag.getIntArray("HeightMap"));
+ outTag.putByte("TerrainPopulated", inTag.getByte("TerrainPopulated"));
+
+ ListTag outSectionTags = new ListTag("Sections");
+ ListTag inSectionTags = inTag.getList("Sections");
+
+ for (int i = 0; i < inSectionTags.size(); i++) {
+ CompoundTag inSectionTag = (CompoundTag) inSectionTags.get(i);
+ byte[] blocks = inSectionTag.getByteArray("Blocks");
+ byte[] blockLight = inSectionTag.getByteArray("BlockLight");
+ byte[] skyLight = inSectionTag.getByteArray("SkyLight");
+ byte[] data = inSectionTag.getByteArray("Data");
+
+ CompoundTag outSectionTag = new CompoundTag();
+ outSectionTag.putByte("Y", inSectionTag.getByte("Y"));
+ outSectionTag.putByteArray("Blocks", reorderByteArray(blocks));
+ outSectionTag.putByteArray("Data", reorderNibbleArray(data));
+ outSectionTag.putByteArray("SkyLight", reorderNibbleArray(skyLight));
+ outSectionTag.putByteArray("BlockLight", reorderNibbleArray(blockLight));
+
+ outSectionTags.add(outSectionTag);
+ }
+ outTag.put("Sections", outSectionTags);
+
+ if (inTag.contains("Biomes")) {
+ outTag.putByteArray("Biomes", inTag.getByteArray("Biomes"));
+ }
+
+ outTag.put("Entities", inTag.getList("Entities"));
+
+ outTag.put("TileEntities", inTag.getList("TileEntities"));
+
+ if (inTag.contains("TileTicks")) {
+ outTag.put("TileTicks", inTag.getList("TileTicks"));
+ }
+ }
+
+ // Code for reorderByteArray and reorderNibbleArray, and processing of the
+ // PMAnvil format is based on https://github.com/Awzaw/AnvilConverter
+ private byte[] reorderByteArray(byte[] myarray) {
+ byte[] result = new byte[16 * 16 * 16];
+ int i = 0;
+
+ for (int x = 0; x < 16; x++) {
+ int zMax = x + 256;
+ for (int z = x; z < zMax; z += 16) {
+ int yMax = z + 4096;
+ for (int y = z; y < yMax; y += 256) {
+ result[i] = myarray[y];
+ i++;
+ }
+ }
+ }
+ return result;
+ }
+
+ private byte[] reorderNibbleArray(byte[] myarray) {
+ byte commonValue = 0x00;
+ byte[] result = new byte[16 * 16 * 8];
+ int i = 0;
+
+ for (int x = 0; x < 8; x++) {
+ for (int z = 0; z < 16; z++) {
+ int zx = ((z << 3) | x);
+ for (int y = 0; y < 8; y++) {
+ int j = ((y << 8) | zx);
+ int j80 = (j | 0x80);
+ if (myarray[j] == commonValue && myarray[j80] == commonValue) {
+ // values are already filled
+ } else {
+ byte i1 = myarray[j];
+ byte i2 = myarray[j80];
+ result[i] = (byte) ((i2 << 4) | (i1 & 0x0f));
+ result[i | 0x80] = (byte) (((i1 & 0xff) >>> 4) | (i2 & 0xf0));
+ }
+ i++;
+ }
+ }
+ i += 128;
+ }
+
+ return result;
+ }
+}