From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Sun, 22 Aug 2021 23:03:33 -0700
Subject: [PATCH] Fix and optimize legacy world conversion

CraftBukkit breaks legacy world conversion in three ways:
- Writes userdata to the path of the userdata folder rather than to
  the correct file inside the aforementioned folder. This causes the
  userdata folder to fail to be created as a file already exists at
  its path.
- Makes changes to how multiworld works, without modifying
  McRegionUpgrader to be aware of these changes.
- Calls methods on Bukkit before the server is initialized.

This patch fixes all of these issues, and also threads the
McRegionUpgrader to improve performance.

1.18 update note: legacy region conversion has been entirely removed from the vanilla server

diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
index 8703f97dc2f392b136c6851aa09b607cbfdfa5de..ade010fe3b62a4624b009c6d665e9909b2d314ac 100644
--- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java
+++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java
@@ -355,14 +355,14 @@ public class OldUsersConverter {
                     }
 
                     private void movePlayerFile(File playerDataFolder, String fileName, String uuid) {
-                        File file5 = new File(file, fileName + ".dat");
+                        File file5 = new File(file, fileName + ".dat"); // Paper - diff on change
                         File file6 = new File(playerDataFolder, uuid + ".dat");
 
                         // CraftBukkit start - Use old file name to seed lastKnownName
                         CompoundTag root = null;
 
                         try {
-                            root = NbtIo.readCompressed(new java.io.FileInputStream(file5));
+                            root = NbtIo.readCompressed(new java.io.FileInputStream(file5)); // Paper - diff on change
                         } catch (Exception exception) {
                             exception.printStackTrace();
                             ServerInternalException.reportInternalException(exception); // Paper
@@ -376,7 +376,7 @@ public class OldUsersConverter {
                             data.putString("lastKnownName", fileName);
 
                             try {
-                                NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
+                                NbtIo.writeCompressed(root, new java.io.FileOutputStream(file5)); // Paper - write to correct path (diff on change)
                             } catch (Exception exception) {
                                 exception.printStackTrace();
                                 ServerInternalException.reportInternalException(exception); // Paper
diff --git a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
index af8a555c777b5abbaa2615d2ff03f03a9a93847e..b794c02ea36bdc901b1f6a160095abb3fcfe9b60 100644
--- a/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
+++ b/src/main/java/net/minecraft/world/level/storage/LevelStorageSource.java
@@ -348,6 +348,12 @@ public class LevelStorageSource {
             });
         }
 
+        // Paper start - copied from vanilla before below CB diff
+        public File getDimensionPathForLegacyConversion(ResourceKey<Level> key) {
+            return DimensionType.getStorageFolder(key, this.levelPath.toFile());
+        }
+        // Paper end
+
         public File getDimensionPath(ResourceKey<Level> key) {
             return LevelStorageSource.getFolder(this.levelPath.toFile(), this.dimensionType); // CraftBukkit
         }
diff --git a/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java b/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
index 87705fc8ee32016bf5ca533eb278bf32df08d3a3..b9bcbcf509ebb59d15082ce0faef8e405c16bdcc 100644
--- a/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
+++ b/src/main/java/net/minecraft/world/level/storage/McRegionUpgrader.java
@@ -35,13 +35,29 @@ public class McRegionUpgrader {
     private static final String MCREGION_EXTENSION = ".mcr";
 
     static boolean convertLevel(LevelStorageSource.LevelStorageAccess storageSession, ProgressListener progressListener) {
+        // Paper start
+        progressListener = new ProgressListener() {
+            @Override
+            public void progressStartNoAbort(net.minecraft.network.chat.Component title) {}
+            @Override
+            public void progressStart(net.minecraft.network.chat.Component title) {}
+            @Override
+            public void progressStage(net.minecraft.network.chat.Component task) {}
+            @Override
+            public void progressStagePercentage(int percentage) {}
+            @Override
+            public void stop() {}
+        };
+        // Paper end
         progressListener.progressStagePercentage(0);
         List<File> list = Lists.newArrayList();
         List<File> list2 = Lists.newArrayList();
         List<File> list3 = Lists.newArrayList();
-        File file = storageSession.getDimensionPath(Level.OVERWORLD);
-        File file2 = storageSession.getDimensionPath(Level.NETHER);
-        File file3 = storageSession.getDimensionPath(Level.END);
+        // Paper start
+        File file = storageSession.getDimensionPathForLegacyConversion(Level.OVERWORLD);
+        File file2 = storageSession.getDimensionPathForLegacyConversion(Level.NETHER);
+        File file3 = storageSession.getDimensionPathForLegacyConversion(Level.END);
+        // Paper end
         LOGGER.info("Scanning folders...");
         addRegionFiles(file, list);
         if (file2.exists()) {
@@ -88,14 +104,58 @@ public class McRegionUpgrader {
     }
 
     private static void convertRegions(RegistryAccess.RegistryHolder registryManager, File directory, Iterable<File> files, BiomeSource biomeSource, int i, int j, ProgressListener progressListener) {
-        for(File file : files) {
-            convertRegion(registryManager, directory, file, biomeSource, i, j, progressListener);
-            ++i;
-            int k = (int)Math.round(100.0D * (double)i / (double)j);
-            progressListener.progressStagePercentage(k);
+        // Paper start - thread this because it's dead simple
+        convertRegionsThreaded(registryManager, directory, files, biomeSource, i, j, progressListener);
+    }
+
+    private static void convertRegionsThreaded(RegistryAccess.RegistryHolder registryManager, File directory, Iterable<File> files, BiomeSource biomeSource, int progress, int total, ProgressListener progressListener) {
+        if (!files.iterator().hasNext()) {
+            return;
         }
 
+        final int threads = Runtime.getRuntime().availableProcessors() / 2;
+        final java.util.concurrent.ExecutorService threadPool = java.util.concurrent.Executors.newFixedThreadPool(Math.max(1, threads), new java.util.concurrent.ThreadFactory() {
+            private final java.util.concurrent.atomic.AtomicInteger threadCounter = new java.util.concurrent.atomic.AtomicInteger();
+
+            @Override
+            public Thread newThread(final Runnable run) {
+                final Thread ret = new Thread(run);
+
+                ret.setName("World upgrader thread for directory " + directory + " #" + this.threadCounter.getAndIncrement());
+                ret.setUncaughtExceptionHandler((thread, throwable) -> {
+                    LOGGER.fatal("Error upgrading world", throwable);
+                });
+
+                return ret;
+            }
+        });
+        final java.util.concurrent.atomic.AtomicInteger converted = new java.util.concurrent.atomic.AtomicInteger(progress);
+
+        final long start = System.nanoTime();
+
+        for (final File file : files) {
+            threadPool.execute(() -> {
+                convertRegion(registryManager, directory, file, biomeSource, 0, total, progressListener);
+                converted.incrementAndGet();
+            });
+        }
+        threadPool.shutdown();
+
+        final java.text.DecimalFormat format = new java.text.DecimalFormat("#0.00");
+        while (!threadPool.isTerminated()) {
+            final int getConverted = converted.get();
+            LOGGER.info("Converting... {}/{} ({}%)", getConverted, total, format.format(100.0D * (double) getConverted / (double) total));
+            try {
+                Thread.sleep(1000L);
+            } catch (final InterruptedException ignored) {}
+        }
+
+        final long end = System.nanoTime();
+
+        final double durationSeconds = Math.ceil((end - start) * 1.0e-9);
+        LOGGER.info("Conversion for {} completed in {}s", directory, durationSeconds);
     }
+    // Paper end
 
     private static void convertRegion(RegistryAccess.RegistryHolder registryManager, File directory, File file, BiomeSource biomeSource, int i, int j, ProgressListener progressListener) {
         String string = file.getName();
diff --git a/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java b/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
index 4d0af984490b556a9911c3b8fdca1e168e6fe932..c20e5d69b4ad8adcdaffb56e4e2a24596ae16edf 100644
--- a/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
+++ b/src/main/java/net/minecraft/world/level/storage/PrimaryLevelData.java
@@ -212,7 +212,7 @@ public class PrimaryLevelData implements ServerLevelData, WorldData {
             levelTag.putUUID("WanderingTraderId", this.wanderingTraderId);
         }
 
-        levelTag.putString("Bukkit.Version", Bukkit.getName() + "/" + Bukkit.getVersion() + "/" + Bukkit.getBukkitVersion()); // CraftBukkit
+        if (Bukkit.getServer() != null) levelTag.putString("Bukkit.Version", Bukkit.getName() + "/" + Bukkit.getVersion() + "/" + Bukkit.getBukkitVersion()); // CraftBukkit // Paper - server may not be started yet
     }
 
     @Override
