diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d34fdb6bad6..73a1e67a9beb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -115,14 +115,14 @@ jobs: - name: Create Paperclip Jar if: fromJSON(steps.determine.outputs.result).action == 'paperclip' - run: ./gradlew createMojmapPaperclipJar --stacktrace + run: ./gradlew createPaperclipJar --stacktrace - name: Upload Paperclip Jar if: fromJSON(steps.determine.outputs.result).action == 'paperclip' uses: actions/upload-artifact@v4 with: name: paper-${{ fromJSON(steps.determine.outputs.result).pr }} - path: paper-server/build/libs/paper-paperclip-*-mojmap.jar + path: paper-server/build/libs/paper-paperclip-*.jar - name: Publish Artifacts if: fromJSON(steps.determine.outputs.result).action == 'paperclip' diff --git a/.gitignore b/.gitignore index e46ae58b1898..44b14519cf11 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ nbactions.xml bin/ dist/ manifest.mf +__pycache__/ # Mac filesystem dust .DS_Store/ diff --git a/gradle.properties b/gradle.properties index 7387810e8ded..fffe319d7082 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ mcVersion=26.1 apiVersion=26.1 # Set to true while updating Minecraft version -updatingMinecraft=true +updatingMinecraft=false org.gradle.configuration-cache=true org.gradle.caching=true diff --git a/paper-api/src/main/java/org/bukkit/Server.java b/paper-api/src/main/java/org/bukkit/Server.java index 720b65117b39..ccd936efb12d 100644 --- a/paper-api/src/main/java/org/bukkit/Server.java +++ b/paper-api/src/main/java/org/bukkit/Server.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.Serializable; import java.net.InetAddress; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -1565,7 +1566,6 @@ default int broadcast(net.kyori.adventure.text.@NotNull Component message) { @NotNull public ConsoleCommandSender getConsoleSender(); - // Paper start /** * Creates a special {@link CommandSender} which redirects command feedback (in the form of chat messages) to the * specified listener. The returned sender will have the same effective permissions as {@link #getConsoleSender()}. @@ -1575,16 +1575,31 @@ default int broadcast(net.kyori.adventure.text.@NotNull Component message) { */ @NotNull public CommandSender createCommandSender(final @NotNull java.util.function.Consumer feedback); - // Paper end /** - * Gets the folder that contains all the various {@link World}s. + * Gets the folder that contains {@link #getLevelDirectory()}. + * + *

This is usually the server's current working directory + * but can be overridden using command line flags (i.e. {@code --universe} or {@code --world-container}).

* - * @return folder that contains all worlds + * @return folder that contains the level directory */ + @ApiStatus.Obsolete @NotNull public File getWorldContainer(); + /** + * Gets the level directory. + * + *

This is the {@code ./world} directory in a fresh default server. Contains player data, dimensions, datapacks, + * and other world data.

+ * + * @return the level directory + */ + @ApiStatus.Experimental + @NotNull + Path getLevelDirectory(); + /** * Gets every player that has ever played on this server. *

diff --git a/paper-api/src/main/java/org/bukkit/WorldCreator.java b/paper-api/src/main/java/org/bukkit/WorldCreator.java index 4b312399eced..171bde1ab971 100644 --- a/paper-api/src/main/java/org/bukkit/WorldCreator.java +++ b/paper-api/src/main/java/org/bukkit/WorldCreator.java @@ -31,11 +31,10 @@ public class WorldCreator { * @param name Name of the world that will be created */ public WorldCreator(@NotNull String name) { - // Paper start - this(name, getWorldKey(name)); + this(name, defaultWorldKey(name)); } - private static NamespacedKey getWorldKey(String name) { + private static NamespacedKey defaultWorldKey(String name) { final String mainLevelName = Bukkit.getUnsafe().getMainLevelName(); if (name.equals(mainLevelName)) { return NamespacedKey.minecraft("overworld"); @@ -58,6 +57,9 @@ public WorldCreator(@NotNull String levelName, @NotNull NamespacedKey worldKey) if (levelName == null || worldKey == null) { throw new IllegalArgumentException("World name and key cannot be null"); } + if (!worldKey.equals(defaultWorldKey(levelName))) { + throw new UnsupportedOperationException("Custom world keys not yet implemented"); + } this.name = levelName; this.seed = (new Random()).nextLong(); this.key = worldKey; @@ -104,7 +106,6 @@ public static WorldCreator ofNameAndKey(@NotNull String levelName, @NotNull Name public static WorldCreator ofKey(@NotNull NamespacedKey worldKey) { return new WorldCreator(worldKey); } - // Paper end /** * Copies the options from the specified world @@ -589,7 +590,6 @@ public static BiomeProvider getBiomeProviderForName(@NotNull String world, @Null return result; } - // Paper start - keep spawn loaded tristate /** * Returns the current intent to keep the world loaded, @see {@link WorldCreator#keepSpawnLoaded(net.kyori.adventure.util.TriState)} * @@ -615,5 +615,4 @@ public net.kyori.adventure.util.TriState keepSpawnLoaded() { public WorldCreator keepSpawnLoaded(@NotNull net.kyori.adventure.util.TriState keepSpawnLoaded) { return this; } - // Paper end - keep spawn loaded tristate } diff --git a/paper-server/build.gradle.kts b/paper-server/build.gradle.kts index 5cef969b4b70..2c03ac384e5a 100644 --- a/paper-server/build.gradle.kts +++ b/paper-server/build.gradle.kts @@ -23,7 +23,7 @@ paperweight { gitFilePatches = false updatingMinecraft { - oldPaperCommit = "7e80cef5198561d0db53406127e5b8bc7af51577" + // oldPaperCommit = "7e80cef5198561d0db53406127e5b8bc7af51577" } } diff --git a/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch b/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch index 71acbb8f4903..6d6e63dceeb0 100644 --- a/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch +++ b/paper-server/patches/features/0002-Allow-Saving-of-Oversized-Chunks.patch @@ -119,10 +119,10 @@ index 728ec122b7af090427cc7511a168336d539835a1..a60a417432cab517fd2fbfd4447c7cb7 + // Paper end } diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 7c6f16fa3de0d344202ec456c648aaebf24a69ca..6b265cc308a14846c9ef3c96aa5daf06e246b8a8 100644 +index 4473503da2459016859b2d72ecd7825df23f4be4..76f83dfa2acbd0840718a022102d820bf65ac1be 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -48,6 +48,43 @@ public final class RegionFileStorage implements AutoCloseable { +@@ -49,6 +49,43 @@ public final class RegionFileStorage implements AutoCloseable { } } @@ -166,7 +166,7 @@ index 7c6f16fa3de0d344202ec456c648aaebf24a69ca..6b265cc308a14846c9ef3c96aa5daf06 public @Nullable CompoundTag read(final ChunkPos pos) throws IOException { // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing RegionFile region = this.getRegionFile(pos, true); -@@ -55,6 +92,12 @@ public final class RegionFileStorage implements AutoCloseable { +@@ -56,6 +93,12 @@ public final class RegionFileStorage implements AutoCloseable { return null; } // CraftBukkit end @@ -179,7 +179,7 @@ index 7c6f16fa3de0d344202ec456c648aaebf24a69ca..6b265cc308a14846c9ef3c96aa5daf06 CompoundTag var4; try (DataInputStream regionChunkInputStream = region.getChunkDataInputStream(pos)) { -@@ -91,6 +134,7 @@ public final class RegionFileStorage implements AutoCloseable { +@@ -92,6 +135,7 @@ public final class RegionFileStorage implements AutoCloseable { } else { try (DataOutputStream output = region.getChunkDataOutputStream(pos)) { NbtIo.write(value, output); diff --git a/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch index 17538a281360..004886a08875 100644 --- a/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch +++ b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch @@ -354,10 +354,10 @@ index 0000000000000000000000000000000000000000..c18823746ab2edcab536cb1589b7720e + } +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 4f31c92c8c3b2757a4103a1b2a0ae158117eebd1..43c47bdd2fdb7731eec5301e20f923d0703a36fc 100644 +index 71446ef19c560f67d22697941973326e2b81bb60..6ea8d1c04afdaf2136dfd5fb3ab1899e8e62fc70 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -591,6 +591,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -599,6 +599,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet profiler.pop(); } @@ -365,7 +365,7 @@ index 4f31c92c8c3b2757a4103a1b2a0ae158117eebd1..43c47bdd2fdb7731eec5301e20f923d0 this.entityTickList .forEach( entity -> { -@@ -1064,12 +1065,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -1072,12 +1073,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet entity.totalEntityAge++; // Paper - age-like counter for all entities profiler.push(entity.typeHolder()::getRegisteredName); profiler.incrementCounter("tickNonPassenger"); @@ -382,7 +382,7 @@ index 4f31c92c8c3b2757a4103a1b2a0ae158117eebd1..43c47bdd2fdb7731eec5301e20f923d0 } // Paper start - log detailed entity tick information } finally { -@@ -1080,7 +1084,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -1088,7 +1092,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet // Paper end - log detailed entity tick information } @@ -391,7 +391,7 @@ index 4f31c92c8c3b2757a4103a1b2a0ae158117eebd1..43c47bdd2fdb7731eec5301e20f923d0 if (entity.isRemoved() || entity.getVehicle() != vehicle) { entity.stopRiding(); } else if (entity instanceof Player || this.entityTickList.contains(entity)) { -@@ -1090,12 +1094,21 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -1098,12 +1102,21 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet ProfilerFiller profiler = Profiler.get(); profiler.push(entity.typeHolder()::getRegisteredName); profiler.incrementCounter("tickPassenger"); @@ -462,7 +462,7 @@ index 6309a615ba2525437758b1fe39c43060ec42d6f8..677b0cadec2270537d868aac7d0acaf7 public void tick() { super.tick(); diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 931a886ab969009c1dc672e3108a0e65f888e0f4..83602c8ee3e2d6064fea66a915f427ffe1584b5a 100644 +index a480a35b23f38680598cb428bd09b7d1db07af6e..9ea33286ac0d4691dbc6b607a18b6688ca1724e9 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -426,6 +426,15 @@ public abstract class Entity @@ -521,7 +521,7 @@ index 931a886ab969009c1dc672e3108a0e65f888e0f4..83602c8ee3e2d6064fea66a915f427ff delta = this.maybeBackOffFromEdge(delta, moverType); Vec3 movement = this.collide(delta); diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index e0e6c0f1ab9609fb64db4dfd436414d7ce12dd12..cbcef6d9d4113e318237df4501839d6870042c8d 100644 +index 9d579b7f17dc38c9e92447ea27ec71a8967a9964..30d518454e7a48b42242184203ab583b19b3e770 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -3385,6 +3385,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin @@ -540,7 +540,7 @@ index e0e6c0f1ab9609fb64db4dfd436414d7ce12dd12..cbcef6d9d4113e318237df4501839d68 public void tick() { super.tick(); diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java -index dab431458aa93349a9f09e8e3502a8b8b11a1653..2c7efcc76da5c2d1596bd15128fa27d77acc152d 100644 +index c7c53ef16e104ebc48aeb8719783fb4c45d39fc2..90eaa775dc134041647618c3b965334a9bb9fd3b 100644 --- a/net/minecraft/world/entity/Mob.java +++ b/net/minecraft/world/entity/Mob.java @@ -211,6 +211,19 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs @@ -673,7 +673,7 @@ index a4f719c1b1b6a8920068ed8969a9e780420eade1..3791ffa6a14a1b2304780382e19f0e38 public void tick() { if (this.getItem().isEmpty()) { diff --git a/net/minecraft/world/entity/npc/villager/Villager.java b/net/minecraft/world/entity/npc/villager/Villager.java -index e7df6fb302abc958608090e84052eccb3190ed95..df50e6ac986cd388f02d405186f2855e42638eac 100644 +index a80e6a23cf29cb44943339101f15565dbc19af1f..f481fd661440f77c98940cbde4a8b95818e4a22c 100644 --- a/net/minecraft/world/entity/npc/villager/Villager.java +++ b/net/minecraft/world/entity/npc/villager/Villager.java @@ -244,11 +244,35 @@ public class Villager extends AbstractVillager implements VillagerDataHolder, Re @@ -816,7 +816,7 @@ index 3a590d4dc980a2912e9cc043d8c8db4cf9d60803..06ab7c48b18c03af494ab10fc2b584ce + } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 8c5725b83a82b4297aecebc5220e9b3160746923..ad39c0822f757216d817934ba18440c228dc6be3 100644 +index 16ca35249e2248500073bb2b12447d2a0e545b95..00be50d2b8de3b5084c14c2e02eaf97f76ed5e49 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -153,6 +153,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable { diff --git a/paper-server/patches/features/0014-Add-Alternate-Current-redstone-implementation.patch b/paper-server/patches/features/0014-Add-Alternate-Current-redstone-implementation.patch index 8d4d7d313a09..7b4548a9e78a 100644 --- a/paper-server/patches/features/0014-Add-Alternate-Current-redstone-implementation.patch +++ b/paper-server/patches/features/0014-Add-Alternate-Current-redstone-implementation.patch @@ -2326,18 +2326,18 @@ index 0000000000000000000000000000000000000000..298076a0db4e6ee6e4775ac43bf749d9 + } +} diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 43c47bdd2fdb7731eec5301e20f923d0703a36fc..75068c07d1892882a85e27e30da8ac9906cf16e7 100644 +index 6ea8d1c04afdaf2136dfd5fb3ab1899e8e62fc70..59ccc7a7c3a93a69e78061d4c091705658f38f48 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -232,6 +232,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet - public final net.minecraft.world.level.storage.LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings; + public final WorldGenSettings worldGenSettings; public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent + private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) @Override public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { -@@ -2434,6 +2435,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -2432,6 +2433,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet return this.debugSynchronizers; } @@ -2352,10 +2352,10 @@ index 43c47bdd2fdb7731eec5301e20f923d0703a36fc..75068c07d1892882a85e27e30da8ac99 return toLevel.dimension() != Level.NETHER || this.getGameRules().get(GameRules.ALLOW_ENTERING_NETHER_USING_PORTALS); } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index ad39c0822f757216d817934ba18440c228dc6be3..52e870443337adbd7a573cbc49a0b094a84a905d 100644 +index 00be50d2b8de3b5084c14c2e02eaf97f76ed5e49..f56bf11ef2202b78635951f511f468231fd7e13a 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java -@@ -1446,6 +1446,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { +@@ -1447,6 +1447,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return this.palettedContainerFactory; } diff --git a/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestMainUtil.java.patch b/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestMainUtil.java.patch deleted file mode 100644 index 6ffa6037b761..000000000000 --- a/paper-server/patches/sources/net/minecraft/gametest/framework/GameTestMainUtil.java.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/net/minecraft/gametest/framework/GameTestMainUtil.java -+++ b/net/minecraft/gametest/framework/GameTestMainUtil.java -@@ -83,7 +_,7 @@ - copyPacks(universePath, packFolder); - } - -- LevelStorageSource.LevelStorageAccess levelStorageSource = LevelStorageSource.createDefault(Paths.get(universePath)).createAccess("gametestworld"); -+ LevelStorageSource.LevelStorageAccess levelStorageSource = LevelStorageSource.createDefault(Paths.get(universePath)).createAccess("gametestworld", net.minecraft.world.level.dimension.LevelStem.OVERWORLD); // Paper - PackRepository packRepository = ServerPacksSource.createPackRepository(levelStorageSource); - MinecraftServer.spin( - thread -> GameTestServer.create( diff --git a/paper-server/patches/sources/net/minecraft/server/Main.java.patch b/paper-server/patches/sources/net/minecraft/server/Main.java.patch index 5f7352813568..3e045b797299 100644 --- a/paper-server/patches/sources/net/minecraft/server/Main.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/Main.java.patch @@ -12,7 +12,7 @@ OptionParser parser = new OptionParser(); OptionSpec nogui = parser.accepts("nogui"); OptionSpec initSettings = parser.accepts("initSettings", "Initializes 'server.properties' and 'eula.txt', then quits"); -@@ -89,41 +_,94 @@ +@@ -89,39 +_,96 @@ parser.printHelpOn(System.err); return; } @@ -39,6 +39,12 @@ - DedicatedServerSettings settings = new DedicatedServerSettings(settingsFile); + DedicatedServerSettings settings = new DedicatedServerSettings(options); // CraftBukkit - CLI argument support settings.forceSave(); ++ // Paper start ++ if (options.has("forceUpgrade") || options.has("recreateRegionFiles")) { ++ LOGGER.error("World upgrade and region file recreation are not yet implemented in Paper 26.1."); ++ return; ++ } ++ // Paper end RegionFileVersion.configure(settings.getProperties().regionFileComression); Path eulaFile = Paths.get("eula.txt"); Eula eula = new Eula(eulaFile); @@ -107,15 +113,10 @@ + } + // Paper end - fix SPIGOT-5824 + Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), universePath, userCacheFile, options); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container -+ // CraftBukkit start -+ String levelName = Optional.ofNullable((String) options.valueOf("world")).orElse(settings.getProperties().levelName); ++ String levelName = Optional.ofNullable((String) options.valueOf("world")).orElse(settings.getProperties().levelName); // CraftBukkit LevelStorageSource levelStorageSource = LevelStorageSource.createDefault(universePath.toPath()); -- LevelStorageSource.LevelStorageAccess access = levelStorageSource.validateAndCreateAccess(levelName); -+ LevelStorageSource.LevelStorageAccess access = levelStorageSource.validateAndCreateAccess(levelName, LevelStem.OVERWORLD); -+ // CraftBukkit end + LevelStorageSource.LevelStorageAccess access = levelStorageSource.validateAndCreateAccess(levelName); Dynamic levelDataTag; - if (access.hasWorldData()) { - Dynamic levelDataUnfixed; @@ -150,12 +_,36 @@ levelDataTag = null; } @@ -154,15 +155,32 @@ WorldStem worldStem; try { -@@ -164,6 +_,7 @@ +@@ -164,17 +_,31 @@ executor -> WorldLoader.load( worldLoadConfig, context -> { + worldLoader.set(context); // CraftBukkit Registry datapackDimensions = context.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM); if (levelDataTag != null) { ++ // Paper start - migrate startup world ++ try { ++ io.papermc.paper.world.migration.WorldFolderMigration.migrateStartupWorld( ++ access, ++ context.datapackWorldgen(), ++ levelName, ++ LevelStem.OVERWORLD, ++ io.papermc.paper.world.PaperWorldLoader.dimensionKey(LevelStem.OVERWORLD) ++ ); ++ } catch (final IOException ex) { ++ throw new UncheckedIOException("Failed to migrate world storage for " + LevelStem.OVERWORLD.identifier(), ex); ++ } ++ // Paper end - migrate startup world LevelDataAndDimensions worldData = LevelStorageSource.getLevelDataAndDimensions( -@@ -174,7 +_,7 @@ +- access, levelDataTag, context.dataConfiguration(), datapackDimensions, context.datapackWorldgen() ++ access, levelDataTag, context.dataConfiguration(), datapackDimensions, context.datapackWorldgen(), net.minecraft.world.level.Level.OVERWORLD // Paper + ); + return new WorldLoader.DataLoadOutput<>( + worldData.worldDataAndGenSettings(), worldData.dimensions().dimensionsRegistryAccess() ); } else { LOGGER.info("No existing world data, creating new world"); @@ -249,12 +267,23 @@ final DedicatedServerSettings settings, final WorldLoader.DataLoadContext context, final Registry datapackDimensions, -@@ -299,7 +_,7 @@ +@@ -299,7 +_,11 @@ final RegistryAccess registryAccess, final boolean recreateRegionFiles ) { - LOGGER.info("Forcing world upgrade!"); ++ throw new UnsupportedOperationException( ++ "World upgrade and region file recreation are not yet implemented in Paper 26.1." ++ ); ++ /* + LOGGER.info("Forcing world upgrade! {}", storageSource.getLevelId()); // CraftBukkit try (WorldUpgrader upgrader = new WorldUpgrader(storageSource, fixerUpper, registryAccess, eraseCache, recreateRegionFiles)) { Component lastStatus = null; +@@ -327,5 +_,6 @@ + } + } + } ++ */ + } + } diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch index 0233b99949f1..43fc90b4508a 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -210,7 +210,8 @@ + if (false && !this.registries.compositeAccess().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD)) { // CraftBukkit - initialised later throw new IllegalStateException("Missing Overworld dimension data"); } else { - this.savedDataStorage = new SavedDataStorage(storageSource.getLevelPath(LevelResource.DATA), fixerUpper, this.registries.compositeAccess()); +- this.savedDataStorage = new SavedDataStorage(storageSource.getLevelPath(LevelResource.DATA), fixerUpper, this.registries.compositeAccess()); ++ this.savedDataStorage = new SavedDataStorage(storageSource.getLevelPath(LevelResource.ROOT).resolve(LevelResource.DATA.id()), fixerUpper, this.registries.compositeAccess()); this.worldData = worldStem.worldDataAndGenSettings().data(); this.worldGenSettings = worldStem.worldDataAndGenSettings().genSettings(); - this.savedDataStorage.set(WorldGenSettings.TYPE, this.worldGenSettings); @@ -299,7 +300,7 @@ if (profiledDuration != null) { profiledDuration.finish(true); } -@@ -422,33 +_,134 @@ +@@ -422,33 +_,139 @@ } } @@ -341,14 +342,13 @@ + // Paper start - rework world loading process + public void createLevel( + LevelStem levelStem, -+ io.papermc.paper.world.PaperWorldLoader.WorldLoadingInfo loadingInfo, -+ LevelStorageSource.LevelStorageAccess levelStorageAccess, ++ io.papermc.paper.world.PaperWorldLoader.WorldLoadingInfoAndData loading, + net.minecraft.world.level.storage.LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings + ) { + final WorldOptions worldOptions = worldDataAndGenSettings.genSettings().options(); -+ final ResourceKey dimensionKey = ResourceKey.create(Registries.DIMENSION, loadingInfo.stemKey().identifier()); -+ final SavedDataStorage savedDataStorage = new SavedDataStorage(levelStorageAccess.getLevelPath(LevelResource.DATA), this.getFixerUpper(), this.registryAccess()); -+ savedDataStorage.set(WorldGenSettings.TYPE, worldDataAndGenSettings.genSettings()); ++ final ResourceKey dimensionKey = loading.info().dimensionKey(); ++ final SavedDataStorage savedDataStorage = new SavedDataStorage(this.storageSource.getDimensionPath(dimensionKey).resolve(LevelResource.DATA.id()), this.getFixerUpper(), this.registryAccess()); ++ savedDataStorage.set(WorldGenSettings.TYPE, new WorldGenSettings(worldDataAndGenSettings.genSettings().options(), worldDataAndGenSettings.genSettings().dimensions())); long seed = worldOptions.seed(); long biomeZoomSeed = BiomeManager.obfuscateSeed(seed); List overworldCustomSpawners = ImmutableList.of( @@ -361,36 +361,40 @@ - this.levels.put(Level.OVERWORLD, overworld); + new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(savedDataStorage) // Paper - save to world data + ); -+ final org.bukkit.generator.ChunkGenerator chunkGenerator = this.server.getGenerator(loadingInfo.name()); -+ org.bukkit.generator.BiomeProvider biomeProvider = this.server.getBiomeProvider(loadingInfo.name()); ++ final org.bukkit.generator.ChunkGenerator chunkGenerator = this.server.getGenerator(loading.data().bukkitName()); ++ org.bukkit.generator.BiomeProvider biomeProvider = this.server.getBiomeProvider(loading.data().bukkitName()); + final org.bukkit.generator.WorldInfo worldInfo = new org.bukkit.craftbukkit.generator.CraftWorldInfo( -+ worldDataAndGenSettings, -+ levelStorageAccess, -+ org.bukkit.World.Environment.getEnvironment(loadingInfo.dimension()), ++ loading.data().bukkitName(), ++ worldDataAndGenSettings.genSettings().options().seed(), ++ worldDataAndGenSettings.data().enabledFeatures(), ++ loading.info().environment(), + levelStem.type().value(), + levelStem.generator(), -+ this.registryAccess() ++ this.registryAccess(), ++ loading.data().uuid() + ); + if (biomeProvider == null && chunkGenerator != null) { + biomeProvider = chunkGenerator.getDefaultBiomeProvider(worldInfo); + } + ServerLevel serverLevel; -+ if (loadingInfo.stemKey() == LevelStem.OVERWORLD) { ++ if (loading.info().stemKey() == LevelStem.OVERWORLD) { + serverLevel = new ServerLevel( + this, + this.executor, -+ levelStorageAccess, -+ worldDataAndGenSettings, ++ this.storageSource, ++ worldDataAndGenSettings.genSettings(), + dimensionKey, + levelStem, + worldDataAndGenSettings.data().isDebugWorld(), + biomeZoomSeed, + overworldCustomSpawners, + true, -+ org.bukkit.World.Environment.getEnvironment(loadingInfo.dimension()), ++ loading.info().stemKey(), ++ loading.info().environment(), + chunkGenerator, + biomeProvider, -+ savedDataStorage ++ savedDataStorage, ++ loading.data() + ); + this.worldData = worldDataAndGenSettings.data(); + this.worldData.setGameType(((net.minecraft.server.dedicated.DedicatedServer) this).getProperties().gameMode.get()); // From DedicatedServer.init @@ -408,26 +412,28 @@ + serverLevel = new ServerLevel( + this, + this.executor, -+ levelStorageAccess, -+ worldDataAndGenSettings, ++ this.storageSource, ++ worldDataAndGenSettings.genSettings(), + dimensionKey, + levelStem, + this.worldData.isDebugWorld(), + biomeZoomSeed, + spawners, + true, -+ org.bukkit.World.Environment.getEnvironment(loadingInfo.dimension()), ++ loading.info().stemKey(), ++ loading.info().environment(), + chunkGenerator, + biomeProvider, -+ savedDataStorage ++ savedDataStorage, ++ loading.data() + ); + } + this.addLevel(serverLevel); + this.initWorld(serverLevel); + } + public void initWorld(ServerLevel overworld) { -+ final net.minecraft.world.level.storage.PrimaryLevelData levelData = overworld.serverLevelData; -+ final WorldOptions worldOptions = overworld.worldDataAndGenSettings.genSettings().options(); ++ final net.minecraft.world.level.storage.ServerLevelData levelData = overworld.serverLevelData; ++ final WorldOptions worldOptions = overworld.worldGenSettings.options(); + final boolean isDebug = this.worldData.isDebugWorld(); + if (overworld.generator != null) { + overworld.getWorld().getPopulators().addAll(overworld.generator.getDefaultPopulators(overworld.getWorld())); @@ -557,26 +563,6 @@ } protected GlobalPos selectLevelLoadFocusPos() { -@@ -597,7 +_,7 @@ - public abstract boolean shouldRconBroadcast(); - - public boolean saveAllChunks(final boolean silent, final boolean flush, final boolean force) { -- this.scoreboard.storeToSaveDataIfDirty(this.getDataStorage().computeIfAbsent(ScoreboardSaveData.TYPE)); -+ if (this.overworld() != null) this.scoreboard.storeToSaveDataIfDirty(this.overworld().getDataStorage().computeIfAbsent(ScoreboardSaveData.TYPE)); // Paper - don't try to save if the overworld was not loaded, generally during early startup failures - boolean result = false; - - for (ServerLevel level : this.getAllLevels()) { -@@ -609,8 +_,8 @@ - result = true; - } - -- GameProfile singleplayerProfile = this.getSingleplayerProfile(); -- this.storageSource.saveDataTag(this.worldData, singleplayerProfile == null ? null : singleplayerProfile.id()); -+ // GameProfile singleplayerProfile = this.getSingleplayerProfile(); // Paper - moved to ServerLevel#save -+ // this.storageSource.saveDataTag(this.worldData, singleplayerProfile == null ? null : singleplayerProfile.id()); // Paper - moved to ServerLevel#save - if (flush) { - this.savedDataStorage.saveAndJoin(); - } else { @@ -649,19 +_,49 @@ this.stopServer(); } @@ -1178,12 +1164,12 @@ - private void updateEffectiveRespawnData() { - LevelData.RespawnData respawnData = this.worldData.overworldData().getRespawnData(); -+ // Paper start - per world respawn data - read "server global" respawn data from overworld dimension reference ++ // Paper start - per world respawn data + public void updateEffectiveRespawnData() { ServerLevel respawnLevel = this.findRespawnDimension(); + LevelData.RespawnData respawnData = respawnLevel.serverLevelData.getRespawnData(); -+ respawnData = respawnData.withLevel(respawnLevel.dimension()); -+ // Paper end - per world respawn data - read "server global" respawn data from overworld dimension reference ++ this.worldData.overworldData().setSpawn(respawnData); // Sync back to level.dat for Paper->Vanilla spawn integrity ++ // Paper end - per world respawn data this.effectiveRespawnData = respawnLevel.getWorldBorderAdjustedRespawnData(respawnData); } @@ -1239,18 +1225,30 @@ - this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate); + // Paper start - per level difficulty, WorldDifficultyChangeEvent + public void setDifficulty(final ServerLevel level, final Difficulty difficulty, final @Nullable CommandSourceStack source, final boolean ignoreLock) { -+ net.minecraft.world.level.storage.PrimaryLevelData worldData = level.serverLevelData; ++ io.papermc.paper.world.saveddata.PaperLevelOverrides worldData = level.serverLevelData; + if (ignoreLock || !worldData.isDifficultyLocked()) { + new io.papermc.paper.event.world.WorldDifficultyChangeEvent( + level.getWorld(), source, org.bukkit.craftbukkit.util.CraftDifficulty.toBukkit(difficulty) + ).callEvent(); + worldData.setDifficulty(worldData.isHardcore() ? Difficulty.HARD : difficulty); + level.setSpawnSettings(level.isSpawningMonsters()); -+ // this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate); ++ level.players().forEach(this::sendDifficultyUpdate); + // Paper end - per level difficulty } } +@@ -1326,6 +_,11 @@ + + public void setDifficultyLocked(final boolean locked) { + this.worldData.setDifficultyLocked(locked); ++ // Paper start - set for all worlds ++ for (final ServerLevel level : this.getAllLevels()) { ++ level.serverLevelData.setDifficultyLocked(locked); ++ } ++ // Paper end - set for all worlds + this.getPlayerList().getPlayers().forEach(this::sendDifficultyUpdate); + } + @@ -1382,10 +_,20 @@ @Override @@ -1417,7 +1415,7 @@ public ServerLevel findRespawnDimension() { - LevelData.RespawnData respawnData = this.getWorldData().overworldData().getRespawnData(); - ResourceKey respawnDimension = respawnData.dimension(); -+ ResourceKey respawnDimension = this.getWorldData().overworldData().getRespawnData().dimension(); // Paper - per world respawn data - read "server global" respawn data from overworld dimension reference ++ ResourceKey respawnDimension = ((net.minecraft.world.level.storage.PrimaryLevelData) this.getWorldData().overworldData()).respawnDimension; // Paper - root cross-world respawn dimension selector ServerLevel respawnLevel = this.getLevel(respawnDimension); return respawnLevel != null ? respawnLevel : this.overworld(); } diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch index bb67c8705f5e..e7a5d5ed4dac 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch @@ -22,6 +22,23 @@ public ServerChunkCache( final ServerLevel level, +@@ -87,6 +_,7 @@ + final boolean syncWrites, + final ChunkStatusUpdateListener chunkStatusListener, + final Supplier overworldDataStorage ++ , final SavedDataStorage savedDataStorage // Paper - initialize SavedDataStorage earlier + ) { + this.level = level; + this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(level); +@@ -99,7 +_,7 @@ + LOGGER.error("Failed to create dimension data storage directory", (Throwable)var14); + } + +- this.savedDataStorage = new SavedDataStorage(dataFolder, fixerUpper, level.registryAccess()); ++ this.savedDataStorage = savedDataStorage; // Paper - initialize SavedDataStorage earlier + this.ticketStorage = this.savedDataStorage.computeIfAbsent(TicketStorage.TYPE); + this.chunkMap = new ChunkMap( + level, @@ -122,6 +_,64 @@ this.clearCache(); } diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch index 3c71105bd808..f52c23c5a592 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -5,23 +5,23 @@ public final ServerChunkCache chunkSource; private final MinecraftServer server; - public final ServerLevelData serverLevelData; -+ public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type ++ public final io.papermc.paper.world.saveddata.PaperLevelOverrides serverLevelData; // Paper - type private final EntityTickList entityTickList = new EntityTickList(); private final ServerWaypointManager waypointManager; private EnvironmentAttributeSystem environmentAttributes; -@@ -221,24 +_,183 @@ +@@ -221,24 +_,192 @@ private final boolean tickTime; private final LevelDebugSynchronizers debugSynchronizers = new LevelDebugSynchronizers(this); + // CraftBukkit start -+ public final LevelStorageSource.LevelStorageAccess levelStorageAccess; ++ private final ResourceKey typeKey; ++ public final String bukkitName; + public final UUID uuid; + public final net.minecraft.server.level.progress.LevelLoadListener levelLoadListener; -+ private final SavedDataStorage savedDataStorage; // Paper - save per-world data in level storage + private final net.minecraft.world.level.gamerules.GameRules gameRules; + private final WeatherData weatherData; + public final net.minecraft.world.level.timers.TimerQueue scheduledEvents; -+ public final net.minecraft.world.level.storage.LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings; ++ public final WorldGenSettings worldGenSettings; + public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent + public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent + @@ -32,7 +32,7 @@ + + @Override + public ResourceKey getTypeKey() { -+ return this.levelStorageAccess.dimensionType; ++ return this.typeKey; + } + + // Paper start @@ -144,31 +144,38 @@ final Executor executor, final LevelStorageSource.LevelStorageAccess levelStorage, - final ServerLevelData levelData, -+ final net.minecraft.world.level.storage.LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings, // CraftBukkit ++ final WorldGenSettings worldGenSettings, // CraftBukkit final ResourceKey dimension, final LevelStem levelStem, final boolean isDebug, final long biomeZoomSeed, final List customSpawners, final boolean tickTime -+ , org.bukkit.World.Environment env, // CraftBukkit -+ org.bukkit.generator.ChunkGenerator gen, // CraftBukkit -+ org.bukkit.generator.BiomeProvider biomeProvider, // CraftBukkit -+ SavedDataStorage savedDataStorage // Paper - pass SavedDataStorage ++ // Paper start - add parameters ++ , ResourceKey typeKey, ++ org.bukkit.World.Environment env, ++ org.bukkit.generator.ChunkGenerator gen, ++ org.bukkit.generator.BiomeProvider biomeProvider, ++ SavedDataStorage savedDataStorage, ++ io.papermc.paper.world.PaperWorldLoader.LoadedWorldData loadedWorldData ++ // Paper end - add parameters ) { - super(levelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates()); + // CraftBukkit start -+ final var levelData = (net.minecraft.world.level.storage.PrimaryLevelData) worldDataAndGenSettings.data(); -+ final GameRules gameRules = new GameRules(levelData.enabledFeatures(), savedDataStorage.computeIfAbsent(net.minecraft.world.level.gamerules.GameRuleMap.TYPE)); ++ final io.papermc.paper.world.saveddata.PaperLevelOverrides levelData = loadedWorldData.levelOverrides(); ++ savedDataStorage.set(io.papermc.paper.world.saveddata.PaperLevelOverrides.TYPE, levelData); ++ savedDataStorage.set(io.papermc.paper.world.saveddata.PaperWorldMetadata.TYPE, new io.papermc.paper.world.saveddata.PaperWorldMetadata(loadedWorldData.uuid())); ++ savedDataStorage.set(io.papermc.paper.world.saveddata.PaperWorldPDC.TYPE, loadedWorldData.pdc() == null ? io.papermc.paper.world.saveddata.PaperWorldPDC.TYPE.constructor().get() : loadedWorldData.pdc()); ++ final GameRules gameRules = new GameRules(server.getWorldData().enabledFeatures(), savedDataStorage.computeIfAbsent(net.minecraft.world.level.gamerules.GameRuleMap.TYPE)); + this.gameRules = gameRules; -+ super(levelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(levelStorage.levelDirectory.path(), levelData.getLevelName(), dimension.identifier(), spigotConfig, server.registryAccess(), gameRules))); // Paper - create paper world configs -+ this.savedDataStorage = savedDataStorage; // Paper - save per-world data in level storage ++ super(levelData, dimension, server.registryAccess(), levelStem.type(), false, isDebug, biomeZoomSeed, server.getMaxChainedNeighborUpdates(), loadedWorldData.bukkitName(), gen, biomeProvider, env, spigotConfig -> server.paperConfigurations.createWorldConfig(io.papermc.paper.configuration.PaperConfigurations.createWorldContextMap(server.storageSource.getDimensionPath(dimension), loadedWorldData.bukkitName(), dimension.identifier(), spigotConfig, server.registryAccess(), gameRules))); // Paper - create paper world configs + this.weatherData = savedDataStorage.computeIfAbsent(WeatherData.TYPE); + this.weatherData.setLevel(this); -+ this.levelStorageAccess = levelStorage; -+ this.uuid = org.bukkit.craftbukkit.util.WorldUUID.getOrCreate(levelStorageAccess.levelDirectory.path().toFile()); ++ this.typeKey = typeKey; ++ this.bukkitName = loadedWorldData.bukkitName(); ++ this.uuid = loadedWorldData.uuid(); + this.levelLoadListener = new net.minecraft.server.level.progress.LoggingLevelLoadListener(false, this); -+ this.worldDataAndGenSettings = worldDataAndGenSettings; ++ this.worldGenSettings = worldGenSettings; + this.scheduledEvents = savedDataStorage.computeIfAbsent(net.minecraft.world.level.timers.TimerQueue.TYPE); + // CraftBukkit end this.tickTime = tickTime; @@ -177,7 +184,9 @@ this.serverLevelData = levelData; ChunkGenerator generator = levelStem.generator(); + // CraftBukkit start -+ this.serverLevelData.setWorld(this); ++ if (loadedWorldData.pdc() != null) { ++ this.getWorld().readBukkitValues(loadedWorldData.pdc().persistentData().toTagCompound()); ++ } + + if (biomeProvider != null) { + net.minecraft.world.level.biome.BiomeSource biomeSource = new org.bukkit.craftbukkit.generator.CustomWorldChunkManager(this.getWorld(), biomeProvider, generator.getBiomeSource()); // Paper - add vanillaBiomeProvider @@ -195,7 +204,7 @@ boolean syncWrites = server.forceSynchronousWrites(); DataFixer fixerUpper = server.getFixerUpper(); EntityPersistentStorage entityStorage = new EntityStorage( -@@ -260,8 +_,8 @@ +@@ -260,16 +_,17 @@ server.getStructureManager(), executor, generator, @@ -206,7 +215,8 @@ syncWrites, this.entityManager::updateChunkStatus, () -> server.overworld().getDataStorage() -@@ -269,7 +_,7 @@ ++ , savedDataStorage // Paper - initialize SavedDataStorage earlier + ); this.chunkSource.getGeneratorState().ensureStructuresGenerated(); this.portalForcer = new PortalForcer(this); if (this.canHaveWeather()) { @@ -215,12 +225,11 @@ } this.raids = this.getDataStorage().computeIfAbsent(Raids.TYPE); -@@ -277,14 +_,14 @@ +@@ -277,14 +_,13 @@ levelData.setGameType(server.getDefaultGameType()); } - WorldGenSettings worldGenSettings = server.getWorldGenSettings(); -+ WorldGenSettings worldGenSettings = worldDataAndGenSettings.genSettings(); // Paper - use world's gen settings WorldOptions options = worldGenSettings.options(); long seed = options.seed(); this.structureCheck = new StructureCheck( @@ -232,18 +241,6 @@ generator, this.chunkSource.randomState(), this, -@@ -292,8 +_,9 @@ - seed, - fixerUpper - ); -- this.structureManager = new StructureManager(this, options, this.structureCheck); -- if (this.dimensionType().hasEnderDragonFight()) { -+ this.structureManager = new StructureManager(this, worldDataAndGenSettings.genSettings().options(), this.structureCheck); // CraftBukkit -+ if (this.dimension() == Level.END && this.dimensionTypeRegistration().is(net.minecraft.world.level.dimension.BuiltinDimensionTypes.END) || env == org.bukkit.World.Environment.THE_END) { // CraftBukkit - Allow to create EnderDragonBattle in default and custom END -+ // this.dragonFight = new EndDragonFight(this, this.serverLevelData.worldGenOptions().seed(), this.serverLevelData.endDragonFightData()); // CraftBukkit // TODO - snapshot - this.dragonFight = this.getDataStorage().computeIfAbsent(EnderDragonFight.TYPE); - this.dragonFight.init(this, seed, BlockPos.ZERO); - } @@ -303,7 +_,15 @@ this.waypointManager = new ServerWaypointManager(); this.environmentAttributes = EnvironmentAttributeSystem.builder().addDefaultLayers(this).build(); @@ -553,28 +550,14 @@ if (progressListener != null) { progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); } -@@ -883,9 +_,21 @@ - this.entityManager.autoSave(); - } - } -+ -+ // CraftBukkit start - moved from MinecraftServer#saveAllChunks -+ com.mojang.authlib.GameProfile singleplayerProfile = this.server.getSingleplayerProfile(); -+ this.levelStorageAccess.saveDataTag(this.worldDataAndGenSettings.data(), singleplayerProfile == null ? null : singleplayerProfile.id()); -+ // CraftBukkit end - moved from MinecraftServer#saveAllChunks - } +@@ -887,6 +_,7 @@ private void saveLevelData(final boolean sync) { -+ // Paper start - save per-world data in level storage -+ if (sync) { -+ this.savedDataStorage.saveAndJoin(); -+ } else { -+ this.savedDataStorage.scheduleSave(); -+ } -+ // Paper end - save per-world data in level storage SavedDataStorage savedDataStorage = this.getChunkSource().getDataStorage(); ++ savedDataStorage.computeIfAbsent(io.papermc.paper.world.saveddata.PaperWorldPDC.TYPE).setFrom((org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer) this.getWorld().getPersistentDataContainer()); // Paper if (sync) { savedDataStorage.saveAndJoin(); + } else { @@ -949,18 +_,40 @@ @Override @@ -958,7 +941,7 @@ final TagKey structureTag, final BlockPos origin, final int maxSearchRadius, final boolean createReference ) { - if (!this.server.getWorldGenSettings().options().generateStructures()) { -+ if (!this.worldDataAndGenSettings.genSettings().options().generateStructures()) { // CraftBukkit ++ if (!this.worldGenSettings.options().generateStructures()) { // CraftBukkit return null; } else { Optional> tag = this.registryAccess().lookupOrThrow(Registries.STRUCTURE).get(structureTag); @@ -1000,21 +983,23 @@ this.getServer().getDataStorage().set(MapItemSavedData.type(id), data); } -@@ -1492,7 +_,19 @@ +@@ -1492,7 +_,21 @@ @Override public void setRespawnData(final LevelData.RespawnData respawnData) { - this.getServer().setRespawnData(respawnData); + // Paper start -+ if (!this.serverLevelData.getRespawnData().positionEquals(respawnData)) { ++ final LevelData.RespawnData previousRespawnData = this.serverLevelData.getRespawnData(); ++ this.serverLevelData.setSpawn(respawnData); ++ if (!previousRespawnData.positionEquals(respawnData)) { + org.bukkit.Location previousLocation = this.getWorld().getSpawnLocation(); -+ this.serverLevelData.setSpawn(respawnData); + this.server.getPlayerList().broadcastAll(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket(respawnData), this.dimension()); + this.server.updateEffectiveRespawnData(); + new org.bukkit.event.world.SpawnChangeEvent(this.getWorld(), previousLocation).callEvent(); + } -+ if (this.server.overworld().serverLevelData.respawnDimension != this.dimension()) { -+ this.server.overworld().serverLevelData.respawnDimension = this.dimension(); ++ final net.minecraft.world.level.storage.PrimaryLevelData overworldData = (net.minecraft.world.level.storage.PrimaryLevelData) this.server.getWorldData(); ++ if (overworldData.respawnDimension != this.dimension()) { ++ overworldData.respawnDimension = this.dimension(); + this.server.updateEffectiveRespawnData(); + } + // Paper end @@ -1038,13 +1023,13 @@ public boolean isFlat() { - return this.server.getWorldData().isFlatWorld(); -+ return this.serverLevelData.isFlatWorld(); // CraftBukkit ++ return this.worldGenSettings.dimensions().get(this.getTypeKey()).map(levelStem -> levelStem.generator() instanceof net.minecraft.world.level.levelgen.FlatLevelSource).orElse(false); // Paper } @Override public long getSeed() { - return this.server.getWorldGenSettings().options().seed(); -+ return this.worldDataAndGenSettings.genSettings().options().seed(); // CraftBukkit ++ return this.worldGenSettings.options().seed(); // CraftBukkit } public @Nullable EnderDragonFight getDragonFight() { @@ -1065,14 +1050,6 @@ return this.entityManager.getEntityGetter(); } -@@ -1785,6 +_,7 @@ - public void close() throws IOException { - super.close(); - this.entityManager.close(); -+ this.savedDataStorage.close(); // Paper - save per-world data in level storage - } - - @Override @@ -1841,8 +_,30 @@ } diff --git a/paper-server/patches/sources/net/minecraft/server/level/progress/LoggingLevelLoadListener.java.patch b/paper-server/patches/sources/net/minecraft/server/level/progress/LoggingLevelLoadListener.java.patch index 25a0dfb51b9d..ef2a4cdde677 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/progress/LoggingLevelLoadListener.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/progress/LoggingLevelLoadListener.java.patch @@ -31,14 +31,14 @@ case PREPARE_GLOBAL_SPAWN: + // Paper start - log dimension + if (this.level != null) { -+ LOGGER.info("Selecting spawn point for world '{}'...", this.level.dimension().identifier()); ++ LOGGER.info("Selecting spawn point for level '{}'...", this.level.dimension().identifier()); + } else { LOGGER.info("Selecting global world spawn..."); + } break; case LOAD_INITIAL_CHUNKS: + if (this.level != null) { -+ LOGGER.info("Loading {} persistent chunks for world '{}'...", totalChunks, this.level.dimension().identifier()); ++ LOGGER.info("Loading {} persistent chunks for level '{}'...", totalChunks, this.level.dimension().identifier()); + } else { LOGGER.info("Loading {} persistent chunks...", totalChunks); + } diff --git a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch index 2ab42ad8acc2..4ffdf8e6e430 100644 --- a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch @@ -200,11 +200,11 @@ + // Paper end - Configurable player collision + // CraftBukkit start - moved down + LOGGER.info( -+ "{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", // CraftBukkit - add world name ++ "{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", // Paper - add world identifier + player.getPlainTextName(), + address, + player.getId(), -+ level.serverLevelData.getLevelName(), // CraftBukkit - add world name ++ level.dimension().identifier(), // Paper - add world identifier + player.getX(), + player.getY(), + player.getZ() diff --git a/paper-server/patches/sources/net/minecraft/util/datafix/DataFixTypes.java.patch b/paper-server/patches/sources/net/minecraft/util/datafix/DataFixTypes.java.patch new file mode 100644 index 000000000000..b4b8c5b0387c --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/util/datafix/DataFixTypes.java.patch @@ -0,0 +1,18 @@ +--- a/net/minecraft/util/datafix/DataFixTypes.java ++++ b/net/minecraft/util/datafix/DataFixTypes.java +@@ -15,6 +_,7 @@ + import net.minecraft.util.datafix.fixes.References; + + public enum DataFixTypes { ++ NONE(null), // Paper - add no-op fix for custom types until we actually need fixers for them + LEVEL(References.LEVEL), + LEVEL_SUMMARY(References.LIGHTWEIGHT_LEVEL), + PLAYER(References.PLAYER), +@@ -81,6 +_,7 @@ + } + + public Dynamic update(final DataFixer fixerUpper, final Dynamic input, final int fromVersion, final int toVersion) { ++ if (this.type == null) return input; // Paper - add no-op fix for custom types until we actually need fixers for them + return fixerUpper.update(this.type, input, fromVersion, toVersion); + } + diff --git a/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch b/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch index f33982d80c30..a68e0a9f981c 100644 --- a/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch +++ b/paper-server/patches/sources/net/minecraft/util/worldupdate/WorldUpgrader.java.patch @@ -1,14 +1,5 @@ --- a/net/minecraft/util/worldupdate/WorldUpgrader.java +++ b/net/minecraft/util/worldupdate/WorldUpgrader.java -@@ -51,7 +_,7 @@ - final boolean recreateRegionFiles - ) { - this.dimensions = registryAccess.lookupOrThrow(Registries.LEVEL_STEM); -- this.levels = this.dimensions.registryKeySet().stream().map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet()); -+ this.levels = java.util.stream.Stream.of(levelSource.dimensionType).map(Registries::levelStemToLevel).collect(Collectors.toUnmodifiableSet()); // CraftBukkit - this.eraseCache = eraseCache; - this.dataFixer = dataFixer; - this.levelStorage = levelSource; @@ -68,7 +_,7 @@ public static CompoundTag getDataFixContextTag(final Registry dimensions, final ResourceKey dimension) { diff --git a/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch b/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch index 79dff7076339..c298ce198584 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/Level.java.patch @@ -73,17 +73,18 @@ protected Level( final WritableLevelData levelData, final ResourceKey dimension, -@@ -141,7 +_,23 @@ +@@ -141,7 +_,24 @@ final boolean isDebug, final long biomeZoomSeed, final int maxChainedNeighborUpdates -+ , org.bukkit.generator.@Nullable ChunkGenerator generator, // Paper ++ , String bukkitName, // Paper ++ org.bukkit.generator.@Nullable ChunkGenerator generator, // Paper + org.bukkit.generator.@Nullable BiomeProvider biomeProvider, // Paper + org.bukkit.World.Environment environment, // Paper + java.util.function.Function paperWorldConfigCreator // Paper - create paper world config ) { -+ this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) levelData).getLevelName()); // Spigot ++ this.spigotConfig = new org.spigotmc.SpigotWorldConfig(bukkitName); // Spigot + this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config + this.generator = generator; + this.world = new CraftWorld((ServerLevel) this, generator, biomeProvider, environment); diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch index adbcbbae2af5..d359e9e44b46 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch @@ -5,7 +5,7 @@ ) { ServerLevel level = context.level(); - if (level.getServer().getWorldGenSettings().options().generateStructures()) { -+ if (level.worldDataAndGenSettings.genSettings().options().generateStructures()) { // CraftBukkit ++ if (level.worldGenSettings.options().generateStructures()) { // CraftBukkit context.generator() .createStructures( level.registryAccess(), diff --git a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch index 9bda26ffe55c..1c6ee4a3c21b 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -29,18 +_,19 @@ +@@ -29,18 +_,20 @@ this.info = info; } @@ -12,7 +12,8 @@ return region; } else { - if (this.regionCache.size() >= 256) { -+ if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - Sanitise RegionFileCache and make configurable ++ int cacheSize = io.papermc.paper.configuration.GlobalConfiguration.get() == null ? 256 : io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize; // Paper - Sanitise RegionFileCache and make configurable - Config not available during initial FileFixerUpper run ++ if (this.regionCache.size() >= cacheSize) { // Paper - Sanitise RegionFileCache and make configurable this.regionCache.removeLast().close(); } diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/LevelData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelData.java.patch index 24b30413a75c..8cf7cb88a4d8 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/storage/LevelData.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelData.java.patch @@ -1,15 +1,11 @@ --- a/net/minecraft/world/level/storage/LevelData.java +++ b/net/minecraft/world/level/storage/LevelData.java -@@ -62,5 +_,25 @@ +@@ -62,5 +_,21 @@ public BlockPos pos() { return this.globalPos.pos(); } + + // Paper start -+ public RespawnData withLevel(ResourceKey dimension) { -+ return new RespawnData(GlobalPos.of(dimension, this.pos()), this.yaw, this.pitch); -+ } -+ + /** + * Equals without checking dimension. + * diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch index c10bcfb94a12..6daa64c0a0d4 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/storage/LevelStorageSource.java.patch @@ -1,81 +1,38 @@ --- a/net/minecraft/world/level/storage/LevelStorageSource.java +++ b/net/minecraft/world/level/storage/LevelStorageSource.java -@@ -169,6 +_,7 @@ - WorldDimensions.Complete dimensions = worldGenSettings.dimensions().bake(datapackDimensions); - Lifecycle lifecycle = dimensions.lifecycle().add(registryAccess.allRegistriesLifecycle()); - PrimaryLevelData worldData = PrimaryLevelData.parse(dataTag, settings, dimensions.specialWorldProperty(), lifecycle); -+ worldData.pdc = (Tag) dataTag.getElement("BukkitValues", null); // CraftBukkit - Add PDC to world - return LevelDataAndDimensions.create(worldData, worldGenSettings, dimensions); - } - } -@@ -394,25 +_,39 @@ - return this.backupDir; +@@ -149,12 +_,13 @@ + final WorldDataConfiguration dataConfiguration, + final Registry datapackDimensions, + final HolderLookup.Provider registryAccess ++ , final ResourceKey dimension // Paper - pass dimension + ) { + if (DataFixers.getFileFixer().requiresFileFixing(NbtUtils.getDataVersion(levelDataTag))) { + throw new IllegalStateException("Cannot get level data without file fixing first"); + } else { + Dynamic dataTag = RegistryOps.injectRegistryContext(levelDataTag, registryAccess); +- WorldGenSettings worldGenSettings = readExistingSavedData(worldAccess, registryAccess, WorldGenSettings.TYPE) ++ WorldGenSettings worldGenSettings = readExistingSavedData(worldAccess, dimension, registryAccess, WorldGenSettings.TYPE) // Paper - pass dimension + .mapOrElse( + Function.identity(), + error -> { +@@ -174,9 +_,9 @@ } -- public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(final String levelId) throws IOException, ContentValidationException { -+ public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(final String levelId, final ResourceKey dimensionType) throws IOException, ContentValidationException { // CraftBukkit + public static DataResult readExistingSavedData( +- final LevelStorageSource.LevelStorageAccess access, final HolderLookup.Provider registryAccess, final SavedDataType savedDataType ++ final LevelStorageSource.LevelStorageAccess access, final ResourceKey dimension, final HolderLookup.Provider registryAccess, final SavedDataType savedDataType // Paper - pass dimension + ) { +- Path dataLocation = savedDataType.id().withSuffix(".dat").resolveAgainst(access.getLevelPath(LevelResource.DATA)); ++ Path dataLocation = savedDataType.id().withSuffix(".dat").resolveAgainst(access.getDimensionPath(dimension).resolve(LevelResource.DATA.id())); // Paper - use dimension scoped data (we 'demote' some data from global and add our own dimension scoped data) + + CompoundTag fileContents; + try { +@@ -396,7 +_,7 @@ + + public LevelStorageSource.LevelStorageAccess validateAndCreateAccess(final String levelId) throws IOException, ContentValidationException { Path levelPath = this.getLevelPath(levelId); - List validationResults = this.worldDirValidator.validateDirectory(levelPath, true); + List validationResults = Boolean.getBoolean("paper.disableWorldSymlinkValidation") ? List.of() : this.worldDirValidator.validateDirectory(levelPath, true); // Paper - add skipping of symlinks scan if (!validationResults.isEmpty()) { throw new ContentValidationException(levelPath, validationResults); } else { -- return new LevelStorageSource.LevelStorageAccess(levelId, levelPath); -+ return new LevelStorageSource.LevelStorageAccess(levelId, levelPath, dimensionType); // CraftBukkit - } - } - -- public LevelStorageSource.LevelStorageAccess createAccess(final String levelId) throws IOException { -+ public LevelStorageSource.LevelStorageAccess createAccess(final String levelId, final ResourceKey dimensionType) throws IOException { // CraftBukkit - Path levelPath = this.getLevelPath(levelId); -- return new LevelStorageSource.LevelStorageAccess(levelId, levelPath); -+ return new LevelStorageSource.LevelStorageAccess(levelId, levelPath, dimensionType); // CraftBukkit - } - - public DirectoryValidator getWorldDirValidator() { - return this.worldDirValidator; - } - -+ // CraftBukkit start -+ public static Path getStorageFolder(Path path, ResourceKey dimensionType) { -+ if (dimensionType == LevelStem.OVERWORLD) { -+ return path; -+ } else if (dimensionType == LevelStem.NETHER) { -+ return path.resolve("DIM-1"); -+ } else if (dimensionType == LevelStem.END) { -+ return path.resolve("DIM1"); -+ } else { -+ return path.resolve("dimensions").resolve(dimensionType.identifier().getNamespace()).resolve(dimensionType.identifier().getPath()); -+ } -+ } -+ // CraftBukkit end -+ - public record LevelCandidates(List levels) implements Iterable { - public boolean isEmpty() { - return this.levels.isEmpty(); -@@ -463,11 +_,15 @@ - public final LevelStorageSource.LevelDirectory levelDirectory; - private final String levelId; - private final Map resources; -+ // CraftBukkit start -+ public final ResourceKey dimensionType; - -- private LevelStorageAccess(final String levelId, final Path path) throws IOException { -+ private LevelStorageAccess(final String levelId, final Path path, final ResourceKey dimensionType) throws IOException { - Objects.requireNonNull(LevelStorageSource.this); - super(); - this.resources = Maps.newHashMap(); -+ this.dimensionType = dimensionType; -+ // CraftBukkit end - this.levelId = levelId; - this.levelDirectory = new LevelStorageSource.LevelDirectory(path); - this.createLock(); -@@ -524,7 +_,7 @@ - } - - public Path getDimensionPath(final ResourceKey name) { -- return DimensionType.getStorageFolder(name, this.levelDirectory.path()); -+ return DimensionType.getStorageFolder(net.minecraft.core.registries.Registries.levelStemToLevel(this.dimensionType), this.levelDirectory.path()); // Paper - } - - private void checkLock() { diff --git a/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch index 7e477fc5f5cd..7d5e36ace649 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/storage/PrimaryLevelData.java.patch @@ -4,32 +4,11 @@ private final PrimaryLevelData.SpecialWorldProperty specialWorldProperty; private final Lifecycle worldGenSettingsLifecycle; private LevelData.RespawnData respawnData; -+ private static final String PAPER_RESPAWN_DIMENSION = "paperSpawnDimension"; // Paper ++ public static final String PAPER_RESPAWN_DIMENSION = "paperSpawnDimension"; // Paper + public net.minecraft.resources.ResourceKey respawnDimension = net.minecraft.world.level.Level.OVERWORLD; // Paper private long gameTime; private final @Nullable UUID singlePlayerUUID; private final int version; -@@ -43,6 +_,20 @@ - private boolean wasModded; - private final Set removedFeatureFlags; - -+ // CraftBukkit start - Add world and pdc -+ private net.minecraft.server.level.ServerLevel world; -+ protected net.minecraft.nbt.Tag pdc; -+ -+ public void setWorld(net.minecraft.server.level.ServerLevel world) { -+ if (this.world != null) { -+ return; -+ } -+ this.world = world; -+ world.getWorld().readBukkitValues(this.pdc); -+ this.pdc = null; -+ } -+ // CraftBukkit end -+ - private PrimaryLevelData( - final @Nullable UUID singlePlayerUUID, - final boolean wasModded, @@ -93,7 +_,7 @@ ) { long gameTime = input.get("Time").asLong(0L); @@ -53,37 +32,35 @@ } @Override -@@ -130,6 +_,8 @@ +@@ -128,20 +_,23 @@ + + writeVersionTag(tag); NbtUtils.addCurrentDataVersion(tag); - tag.putInt("GameType", this.settings.gameType().getId()); - tag.store("spawn", LevelData.RespawnData.CODEC, this.respawnData); +- tag.putInt("GameType", this.settings.gameType().getId()); +- tag.store("spawn", LevelData.RespawnData.CODEC, this.respawnData); +- tag.putLong("Time", this.gameTime); ++ tag.putInt("GameType", this.settings.gameType().getId()); // Paper - diff on change - PaperLevelOverrides.createFromRawLevelData + // TODO - snapshot - Missing WorldGenSettings.encode diff ++ tag.store("spawn", LevelData.RespawnData.CODEC, this.respawnData); // Paper - diff on change - PaperLevelOverrides.createFromRawLevelData + tag.store(PAPER_RESPAWN_DIMENSION, net.minecraft.world.level.Level.RESOURCE_KEY_CODEC, this.respawnDimension); // Paper - tag.putLong("Time", this.gameTime); ++ tag.putLong("Time", this.gameTime); // Paper - diff on change - PaperLevelOverrides.createFromRawLevelData writeLastPlayed(tag); tag.putString("LevelName", this.settings.levelName()); -@@ -142,6 +_,8 @@ + tag.putInt("version", 19133); + tag.putBoolean("allowCommands", this.settings.allowCommands()); +- tag.putBoolean("initialized", this.initialized); +- tag.store("difficulty_settings", LevelSettings.DifficultySettings.CODEC, this.settings.difficultySettings()); ++ tag.putBoolean("initialized", this.initialized); // Paper - diff on change - PaperLevelOverrides.createFromRawLevelData ++ tag.store("difficulty_settings", LevelSettings.DifficultySettings.CODEC, this.settings.difficultySettings()); // Paper - diff on change - PaperLevelOverrides.createFromRawLevelData + if (singlePlayerUUID != null) { + tag.storeNullable("singleplayer_uuid", UUIDUtil.CODEC, singlePlayerUUID); } tag.store(WorldDataConfiguration.MAP_CODEC, this.settings.dataConfiguration()); + tag.putString("Bukkit.Version", org.bukkit.Bukkit.getName() + "/" + org.bukkit.Bukkit.getVersion() + "/" + org.bukkit.Bukkit.getBukkitVersion()); // CraftBukkit -+ this.world.getWorld().storeBukkitValues(tag); // CraftBukkit - add pdc } public static void writeLastPlayed(final CompoundTag tag) { -@@ -249,6 +_,12 @@ - @Override - public void setDifficulty(final Difficulty difficulty) { - this.settings = this.settings.withDifficulty(difficulty); -+ // CraftBukkit start -+ net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket packet = new net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket(this.getDifficulty(), this.isDifficultyLocked()); -+ for (net.minecraft.server.level.ServerPlayer player : (java.util.List) (java.util.List) this.world.players()) { -+ player.connection.send(packet); -+ } -+ // CraftBukkit end - } - - @Override @@ -322,6 +_,14 @@ public LevelSettings getLevelSettings() { return this.settings.copy(); diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java index 889051d2d6e6..001daf9f0018 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/Configurations.java @@ -276,7 +276,7 @@ private UnaryOperator applyObjectMapperFactory(final Objec } public Path getWorldConfigFile(ServerLevel level) { - return level.levelStorageAccess.levelDirectory.path().resolve(this.worldConfigFileName); + return level.getServer().storageSource.getDimensionPath(level.dimension()).resolve(this.worldConfigFileName); } public static class ContextMap { diff --git a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java index e0491ef5cfc0..93027a0e7e59 100644 --- a/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +++ b/paper-server/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java @@ -347,7 +347,7 @@ public void reloadConfigs(MinecraftServer server) { } private static ContextMap createWorldContextMap(ServerLevel level) { - return createWorldContextMap(level.levelStorageAccess.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().identifier(), level.spigotConfig, level.registryAccess(), level.getGameRules()); + return createWorldContextMap(level.getServer().storageSource.getDimensionPath(level.dimension()), level.bukkitName, level.dimension().identifier(), level.spigotConfig, level.registryAccess(), level.getGameRules()); } public static ContextMap createWorldContextMap(final Path dir, final String levelName, final Identifier worldKey, final SpigotWorldConfig spigotConfig, final RegistryAccess registryAccess, final GameRules gameRules) { diff --git a/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java b/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java index 8e9ed8ca960e..cd3e7cc8cf08 100644 --- a/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java +++ b/paper-server/src/main/java/io/papermc/paper/world/PaperWorldLoader.java @@ -1,171 +1,142 @@ package io.papermc.paper.world; -import com.google.common.io.Files; -import com.mojang.logging.LogUtils; -import com.mojang.serialization.Dynamic; +import io.papermc.paper.world.migration.WorldFolderMigration; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; +import io.papermc.paper.world.saveddata.PaperWorldMetadata; +import io.papermc.paper.world.saveddata.PaperWorldPDC; +import java.io.IOException; +import java.util.Locale; +import java.util.UUID; import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.NbtException; -import net.minecraft.nbt.ReportedNbtException; +import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.Main; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.datafix.DataFixers; -import net.minecraft.util.worldupdate.UpgradeProgress; +import net.minecraft.world.level.Level; import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.WorldGenSettings; import net.minecraft.world.level.storage.LevelDataAndDimensions; +import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; -import net.minecraft.world.level.storage.LevelSummary; import net.minecraft.world.level.storage.PrimaryLevelData; -import net.minecraft.world.level.validation.ContentValidationException; -import org.apache.commons.io.FileUtils; +import net.minecraft.world.level.storage.SavedDataStorage; +import org.bukkit.NamespacedKey; import org.bukkit.World; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import java.io.File; -import java.io.IOException; -import java.util.Locale; -public record PaperWorldLoader(MinecraftServer server, String levelId) { - private static final Logger LOGGER = LogUtils.getClassLogger(); +import static java.util.Objects.requireNonNull; +@NullMarked +public record PaperWorldLoader(MinecraftServer server, String levelId) { public static PaperWorldLoader create(final MinecraftServer server, final String levelId) { return new PaperWorldLoader(server, levelId); } public record WorldLoadingInfo( - int dimension, - String name, - String worldType, + World.Environment environment, ResourceKey stemKey, + ResourceKey dimensionKey, boolean enabled ) {} - private WorldLoadingInfo getWorldInfo( - final String levelId, - final LevelStem stem - ) { + public record LoadedWorldData( + String bukkitName, + UUID uuid, + @Nullable PaperWorldPDC pdc, + PaperLevelOverrides levelOverrides + ) {} + + public record WorldLoadingInfoAndData(WorldLoadingInfo info, LoadedWorldData data) {} + + private @Nullable WorldLoadingInfoAndData getWorldInfoAndData(final LevelStem stem) { + final WorldLoadingInfo info = this.getWorldInfo(stem); + if (!info.enabled()) { + return null; + } + + final String defaultName = defaultWorldName(this.levelId, info.stemKey()); + + try { + WorldFolderMigration.migrateStartupWorld(this.server.storageSource, this.server.registryAccess(), defaultName, info.stemKey(), info.dimensionKey()); + } catch (final IOException ex) { + throw new RuntimeException("Failed to migrate world storage for " + defaultName, ex); + } + + final LoadedWorldData loadedWorldData = loadWorldData( + this.server, + info.dimensionKey(), + defaultName + ); + + return new WorldLoadingInfoAndData(info, loadedWorldData); + } + + public static ResourceKey dimensionKey(final ResourceKey stemKey) { + return ResourceKey.create(Registries.DIMENSION, stemKey.identifier()); + } + + public static ResourceKey dimensionKey(final NamespacedKey key) { + return ResourceKey.create(Registries.DIMENSION, Identifier.fromNamespaceAndPath(key.namespace(), key.value())); + } + + private WorldLoadingInfo getWorldInfo(final LevelStem stem) { final ResourceKey stemKey = this.server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getResourceKey(stem).orElseThrow(); - int dimension = 0; + final ResourceKey dimensionKey = dimensionKey(stemKey); boolean enabled = true; + final World.Environment environment; if (stemKey == LevelStem.NETHER) { - dimension = -1; + environment = World.Environment.NETHER; enabled = this.server.server.getAllowNether(); } else if (stemKey == LevelStem.END) { - dimension = 1; + environment = World.Environment.THE_END; enabled = this.server.server.getAllowEnd(); - } else if (stemKey != LevelStem.OVERWORLD) { - dimension = -999; - } - String worldType = dimension == -999 - ? stemKey.identifier().getNamespace() + "_" + stemKey.identifier().getPath() - : World.Environment.getEnvironment(dimension).toString().toLowerCase(Locale.ROOT); - String name = stemKey == LevelStem.OVERWORLD - ? levelId - : levelId + "_" + worldType; - return new WorldLoadingInfo(dimension, name, worldType, stemKey, enabled); - } - - private void migrateWorldFolder(final WorldLoadingInfo info) { - // Migration of old CB world folders... - if (info.dimension() == 0) { - return; + } else if (stemKey == LevelStem.OVERWORLD) { + environment = World.Environment.NORMAL; + } else { + environment = World.Environment.CUSTOM; } - File newWorld = LevelStorageSource.getStorageFolder(new File(info.name()).toPath(), info.stemKey()).toFile(); - File oldWorld = LevelStorageSource.getStorageFolder(new File(this.levelId).toPath(), info.stemKey()).toFile(); - File oldLevelDat = new File(new File(this.levelId), "level.dat"); // The data folders exist on first run as they are created in the PersistentCollection constructor above, but the level.dat won't - - if (!newWorld.isDirectory() && oldWorld.isDirectory() && oldLevelDat.isFile()) { - LOGGER.info("---- Migration of old " + info.worldType() + " folder required ----"); - LOGGER.info("Unfortunately due to the way that Minecraft implemented multiworld support in 1.6, Bukkit requires that you move your " + info.worldType() + " folder to a new location in order to operate correctly."); - LOGGER.info("We will move this folder for you, but it will mean that you need to move it back should you wish to stop using Bukkit in the future."); - LOGGER.info("Attempting to move " + oldWorld + " to " + newWorld + "..."); - - if (newWorld.exists()) { - LOGGER.warn("A file or folder already exists at " + newWorld + "!"); - LOGGER.info("---- Migration of old " + info.worldType() + " folder failed ----"); - } else if (newWorld.getParentFile().mkdirs()) { - if (oldWorld.renameTo(newWorld)) { - LOGGER.info("Success! To restore " + info.worldType() + " in the future, simply move " + newWorld + " to " + oldWorld); - // Migrate world data too. - try { - Files.copy(oldLevelDat, new File(new File(info.name()), "level.dat")); - FileUtils.copyDirectory(new File(new File(this.levelId), "data"), new File(new File(info.name()), "data")); - } catch (IOException exception) { - LOGGER.warn("Unable to migrate world data."); - } - LOGGER.info("---- Migration of old " + info.worldType() + " folder complete ----"); - } else { - LOGGER.warn("Could not move folder " + oldWorld + " to " + newWorld + "!"); - LOGGER.info("---- Migration of old " + info.worldType() + " folder failed ----"); - } - } else { - LOGGER.warn("Could not create path for " + newWorld + "!"); - LOGGER.info("---- Migration of old " + info.worldType() + " folder failed ----"); - } - } + return new WorldLoadingInfo(environment, stemKey, dimensionKey, enabled); } - // Loosely modeled on code in net.minecraft.server.Main - public void loadInitialWorlds() { - for (final LevelStem stem : this.server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)) { - final WorldLoadingInfo info = this.getWorldInfo(this.levelId, stem); - this.migrateWorldFolder(info); - if (!info.enabled()) { - continue; - } + public static LoadedWorldData loadWorldData( + final MinecraftServer server, + final ResourceKey dimension, + final String defaultName + ) { + final var storageSource = server.storageSource; + final var registryAccess = server.registryAccess(); - LevelStorageSource.LevelStorageAccess levelStorageAccess = this.server.storageSource; - if (info.dimension() != 0) { - try { - levelStorageAccess = LevelStorageSource.createDefault(this.server.server.getWorldContainer().toPath()).validateAndCreateAccess(info.name(), info.stemKey()); - } catch (IOException | ContentValidationException ex) { - throw new RuntimeException(ex); - } - } + final SavedDataStorage tempStorage = new SavedDataStorage(storageSource.getDimensionPath(dimension).resolve(LevelResource.DATA.id()), DataFixers.getDataFixer(), registryAccess); + final PaperWorldMetadata metadata = tempStorage.get(PaperWorldMetadata.TYPE); + final PaperWorldPDC pdc = tempStorage.get(PaperWorldPDC.TYPE); + final PaperLevelOverrides levelOverrides = tempStorage.get(PaperLevelOverrides.TYPE); - final LevelDataResult levelData = getLevelData(levelStorageAccess); - if (levelData.fatalError) { - return; - } + final LoadedWorldData data = new LoadedWorldData( + defaultName, + metadata == null ? UUID.randomUUID() : metadata.uuid(), + pdc, + levelOverrides == null ? PaperLevelOverrides.createFromLiveLevelData((PrimaryLevelData) server.getWorldData()) : levelOverrides + ); - final LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings; - if (levelData.dataTag == null) { - worldDataAndGenSettings = Main.createNewWorldData( - ((DedicatedServer) this.server).settings, - this.server.worldLoaderContext, - this.server.worldLoaderContext.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM), - this.server.isDemo(), - this.server.options.has("bonusChest") - ).cookie(); - } else { - worldDataAndGenSettings = LevelStorageSource.getLevelDataAndDimensions( - levelStorageAccess, - levelData.dataTag, - this.server.worldLoaderContext.dataConfiguration(), - this.server.worldLoaderContext.datapackDimensions().lookupOrThrow(Registries.LEVEL_STEM), - this.server.worldLoaderContext.datapackWorldgen() - ).worldDataAndGenSettings(); - } + data.levelOverrides().attach((PrimaryLevelData) server.getWorldData(), dimension); - final var primaryLevelData = ((PrimaryLevelData) worldDataAndGenSettings.data()); - primaryLevelData.checkName(info.name()); // CraftBukkit - Migration did not rewrite the level.dat; This forces 1.8 to take the last loaded world as respawn (in this case the end) - primaryLevelData.setModdedInfo(this.server.getServerModName(), this.server.getModdedStatus().shouldReportAsModified()); + return data; + } - if (this.server.options.has("forceUpgrade")) { - Main.forceUpgrade( - levelStorageAccess, - DataFixers.getDataFixer(), - this.server.options.has("eraseCache"), - () -> true, - this.server.registryAccess(), - this.server.options.has("recreateRegionFiles") - ); + public void loadInitialWorlds() { + final var levelStemRegistry = this.server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM); + final boolean hasWorldData = this.server.storageSource.hasWorldData(); + final LevelStem overworldStem = requireNonNull(levelStemRegistry.getValue(LevelStem.OVERWORLD), "Overworld stem missing"); + this.loadInitialWorld(overworldStem, hasWorldData); + for (final LevelStem stem : levelStemRegistry) { + if (stem == overworldStem) { + continue; } - - this.server.createLevel(stem, info, levelStorageAccess, worldDataAndGenSettings); + this.loadInitialWorld(stem, hasWorldData); } // ((DedicatedServer) this.server).forceDifficulty(); @@ -175,38 +146,59 @@ public void loadInitialWorlds() { } } - public record LevelDataResult(@Nullable Dynamic dataTag, boolean fatalError) {} + private void loadInitialWorld(final LevelStem stem, final boolean hasWorldData) { + final WorldLoadingInfoAndData loading = this.getWorldInfoAndData(stem); + if (loading == null) { + return; + } - // Based on code in net.minecraft.server.Main - public static LevelDataResult getLevelData( - final LevelStorageSource.LevelStorageAccess access - ) { - Dynamic levelDataTag; - if (access.hasWorldData()) { - Dynamic levelDataUnfixed; - try { - levelDataUnfixed = access.getUnfixedDataTagWithFallback(); - } catch (NbtException | ReportedNbtException | IOException var39) { - LOGGER.error("Failed to load world data. World files may be corrupted. Shutting down.", var39); - return new LevelDataResult(null, true); - } + final WorldGenSettings worldGenSettings = !hasWorldData + ? this.server.getWorldGenSettings() + : loadWorldGenSettings( + this.server.storageSource, + this.server.worldLoaderContext.datapackWorldgen(), + loading.info().dimensionKey() + ); + final var worldDataAndGenSettings = new LevelDataAndDimensions.WorldDataAndGenSettings(this.server.getWorldData(), worldGenSettings); + + if (loading.info().dimensionKey() == Level.OVERWORLD) { + final var primaryLevelData = ((PrimaryLevelData) this.server.getWorldData()); + primaryLevelData.checkName(loading.data().bukkitName()); + primaryLevelData.setModdedInfo(this.server.getServerModName(), this.server.getModdedStatus().shouldReportAsModified()); + } - LevelSummary summary = access.fixAndGetSummaryFromTag(levelDataUnfixed); - if (summary.requiresManualConversion()) { - LOGGER.info("This world must be opened in an older version (like 1.6.4) to be safely converted"); - return new LevelDataResult(null, true); - } + if (this.server.options.has("forceUpgrade")) { + Main.forceUpgrade(this.server.storageSource, DataFixers.getDataFixer(), this.server.options.has("eraseCache"), () -> true, this.server.registryAccess(), this.server.options.has("recreateRegionFiles")); + } - if (!summary.isCompatible()) { - LOGGER.info("This world was created by an incompatible version."); - return new LevelDataResult(null, true); - } + this.server.createLevel(stem, loading, worldDataAndGenSettings); + } - levelDataTag = DataFixers.getFileFixer().fix(access, levelDataUnfixed, new UpgradeProgress()); - } else { - return new LevelDataResult(null, false); + public static WorldGenSettings loadWorldGenSettings( + final LevelStorageSource.LevelStorageAccess access, final net.minecraft.core.HolderLookup.Provider registryAccess, final ResourceKey dimension + ) { + return LevelStorageSource.readExistingSavedData(access, dimension, registryAccess, WorldGenSettings.TYPE) + .getOrThrow(err -> new IllegalStateException("Unable to read or access the world gen settings file for dimension " + dimension.identifier() + ". " + err)); + } + + private static String defaultWorldName(final String levelId, final ResourceKey stemKey) { + if (stemKey == LevelStem.OVERWORLD) { + return levelId; } + return levelId + "_" + worldType(stemKey); + } - return new LevelDataResult(levelDataTag, false); + private static String worldType(final ResourceKey stemKey) { + if (stemKey == LevelStem.NETHER) { + return World.Environment.NETHER.toString().toLowerCase(Locale.ROOT); + } + if (stemKey == LevelStem.END) { + return World.Environment.THE_END.toString().toLowerCase(Locale.ROOT); + } + if (stemKey == LevelStem.OVERWORLD) { + return World.Environment.NORMAL.toString().toLowerCase(Locale.ROOT); + } + return stemKey.identifier().getNamespace() + "_" + stemKey.identifier().getPath(); } + } diff --git a/paper-server/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java b/paper-server/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java new file mode 100644 index 000000000000..761ce73d2047 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/migration/LegacyCraftBukkitWorldMigration.java @@ -0,0 +1,255 @@ +package io.papermc.paper.world.migration; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; +import io.papermc.paper.world.saveddata.PaperWorldMetadata; +import io.papermc.paper.world.saveddata.PaperWorldPDC; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.UUID; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.entity.raid.Raids; +import net.minecraft.world.level.TicketStorage; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.dimension.end.EnderDragonFight; +import net.minecraft.world.level.gamerules.GameRuleMap; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.saveddata.SavedDataType; +import net.minecraft.world.level.saveddata.WanderingTraderData; +import net.minecraft.world.level.saveddata.WeatherData; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.SavedDataStorage; +import net.minecraft.world.level.timers.TimerQueue; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +import static java.util.Objects.requireNonNullElseGet; + +@NullMarked +final class LegacyCraftBukkitWorldMigration { + private static final Logger LOGGER = LogUtils.getClassLogger(); + private static final String[] WORLD_BORDER_RELATIVE_CANDIDATES = {"minecraft/world_border.dat", "world_border.dat"}; + + private final WorldMigrationContext context; + private final Path sourceRoot; + private final Path targetDataRoot; + private final Path targetDimensionPath; + private final HolderLookup.Provider registryAccess; + private @Nullable Path initialExplicitWorldBorder; + private List sourceDataRoots = List.of(); + + static void migrate(final WorldMigrationContext context) throws IOException { + new LegacyCraftBukkitWorldMigration(context).run(); + } + + static void migrateApiWorld(final WorldMigrationContext context) throws IOException { + final Path sourceRoot = context.rootAccess().parent().getLevelPath(context.worldName()); + if (!Files.isDirectory(sourceRoot)) { + return; + } + if (!Files.isRegularFile(sourceRoot.resolve(LevelResource.LEVEL_DATA_FILE.id())) && !Files.isRegularFile(sourceRoot.resolve(LevelResource.OLD_LEVEL_DATA_FILE.id()))) { + return; + } + migrate(context); + } + + private LegacyCraftBukkitWorldMigration(final WorldMigrationContext context) { + this.context = context; + this.sourceRoot = context.rootAccess().parent().getLevelPath(context.worldName()); + this.targetDataRoot = context.targetDataRoot(); + this.targetDimensionPath = context.targetDimensionPath(); + this.registryAccess = context.registryAccess(); + } + + private void run() throws IOException { + LOGGER.info("Starting legacy CraftBukkit import for world '{}' ({})", this.context.worldName(), this.context.dimensionKey().identifier()); + try (final var sourceAccess = this.context.rootAccess().parent().createAccess(this.context.worldName())) { + Files.createDirectories(this.targetDataRoot); + + final List initialDimensionRoots = this.locateDimensionRoots(); + this.initialExplicitWorldBorder = this.findExplicitFile(this.explicitDataRoots(initialDimensionRoots), WORLD_BORDER_RELATIVE_CANDIDATES); + for (final Path sourceDimensionRoot : initialDimensionRoots) { + WorldMigrationSupport.migrateDimensionDirectories(sourceDimensionRoot, this.targetDimensionPath); + } + + WorldMigrationSupport.migratePaperWorldConfig(this.sourceRoot, this.targetDimensionPath); + + final var levelDataResult = WorldMigrationSupport.readFixedLevelData(sourceAccess); + if (levelDataResult.fatalError()) { + throw new IOException("Failed to read level data for world migration"); + } + + this.sourceDataRoots = this.locateSavedDataRoots(); + + final SavedDataStorage tempStorage = new SavedDataStorage(this.targetDataRoot, DataFixers.getDataFixer(), this.registryAccess); + this.migrateSharedSavedData(); + this.migrateLegacyCraftBukkitPaperData(tempStorage, levelDataResult.dataTag()); + tempStorage.saveAndJoin(); + } + deleteMigratedSeparateRoot(this.sourceRoot); + LOGGER.info("Completed legacy CraftBukkit import for world '{}' ({})", this.context.worldName(), this.context.dimensionKey().identifier()); + } + + private void migrateSharedSavedData() throws IOException { + this.copySavedDataIfPresent(GameRuleMap.TYPE); + this.copySavedDataIfPresent(WeatherData.TYPE); + this.copySavedDataIfPresent(TimerQueue.TYPE); + this.copySavedDataIfPresent(WanderingTraderData.TYPE); + this.copySavedDataIfPresent(TicketStorage.TYPE); + this.copySavedDataIfPresent(Raids.TYPE); + this.copySavedDataIfPresent(WorldGenSettings.TYPE); + this.migrateLegacyWorldBorder(); + + if (this.context.stemKey() == LevelStem.END) { + this.copySavedDataIfPresent(EnderDragonFight.TYPE); + } else { + Files.deleteIfExists(WorldMigrationSupport.savedDataPath(this.targetDataRoot, EnderDragonFight.TYPE)); + } + } + + static List rootOwnedDataRoots(final Path sourceRoot) { + return List.of( + WorldMigrationSupport.getStorageFolder(LevelStem.OVERWORLD.identifier(), sourceRoot).resolve(LevelResource.DATA.id()), + sourceRoot.resolve(LevelResource.DATA.id()) + ); + } + + private void migrateLegacyWorldBorder() throws IOException { + if (this.initialExplicitWorldBorder != null && Files.isRegularFile(this.initialExplicitWorldBorder)) { + WorldMigrationSupport.copySavedDataFileIfPresent(this.targetDataRoot, this.initialExplicitWorldBorder, WorldBorder.TYPE, true); + return; + } + + final Path rootOwnedBorder = this.findExplicitFile(rootOwnedDataRoots(this.sourceRoot), WORLD_BORDER_RELATIVE_CANDIDATES); + if (rootOwnedBorder != null) { + WorldMigrationSupport.copySavedDataFileIfPresent(this.targetDataRoot, rootOwnedBorder, WorldBorder.TYPE, true); + return; + } + + this.copySavedDataIfPresent(WorldBorder.TYPE); + } + + private List locateDimensionRoots() { + final ResourceKey stemKey = this.context.stemKey(); + final LinkedHashSet roots = new LinkedHashSet<>(); + roots.add(WorldMigrationSupport.getStorageFolder(stemKey.identifier(), this.sourceRoot)); + roots.add(DimensionType.getStorageFolder(this.context.dimensionKey(), this.sourceRoot)); + if (hasSourceContent(this.sourceRoot)) { + roots.add(this.sourceRoot); + } + + if (stemKey == LevelStem.NETHER) { + roots.add(this.sourceRoot.resolve("DIM-1")); + } else if (stemKey == LevelStem.END) { + roots.add(this.sourceRoot.resolve("DIM1")); + } + + roots.removeIf(path -> !Files.isDirectory(path)); + return List.copyOf(roots); + } + + private static boolean hasSourceContent(final Path root) { + if (Files.isDirectory(root.resolve(LevelResource.DATA.id()))) { + return true; + } + if (Files.isRegularFile(root.resolve(WorldMigrationSupport.PAPER_WORLD_CONFIG)) || Files.isRegularFile(root.resolve("uid.dat"))) { + return true; + } + for (final String directory : WorldMigrationSupport.DIMENSION_DIRECTORIES) { + if (Files.isDirectory(root.resolve(directory))) { + return true; + } + } + return false; + } + + private List locateSavedDataRoots() { + final LinkedHashSet dataRoots = new LinkedHashSet<>(); + dataRoots.addAll(this.explicitDataRoots(this.locateDimensionRoots())); + dataRoots.addAll(rootOwnedDataRoots(this.sourceRoot)); + return List.copyOf(dataRoots); + } + + private void copySavedDataIfPresent( + final SavedDataType type + ) throws IOException { + WorldMigrationSupport.copySavedDataIfPresent(this.sourceDataRoots, this.targetDataRoot, type, true); + } + + private static @Nullable UUID readLegacyUuid(final Path sourceRoot) { + final Path fileId = sourceRoot.resolve("uid.dat"); + if (!Files.isRegularFile(fileId)) { + return null; + } + + try (DataInputStream inputStream = new DataInputStream(Files.newInputStream(fileId))) { + return new UUID(inputStream.readLong(), inputStream.readLong()); + } catch (final IOException ex) { + LOGGER.warn("Failed to read {}", fileId, ex); + return null; + } + } + + private static void deleteMigratedSeparateRoot(final Path sourceRoot) throws IOException { + if (!Files.exists(sourceRoot)) { + return; + } + + try (final var paths = Files.walk(sourceRoot)) { + for (final Path path : paths.sorted(java.util.Comparator.reverseOrder()).toList()) { + Files.deleteIfExists(path); + } + } + } + + private @Nullable Path findExplicitFile( + final List dataRoots, + final String... relativeCandidates + ) { + for (final Path dataRoot : dataRoots) { + for (final String relativeCandidate : relativeCandidates) { + final Path source = dataRoot.resolve(relativeCandidate); + if (Files.isRegularFile(source)) { + return source; + } + } + } + return null; + } + + private void migrateLegacyCraftBukkitPaperData( + final SavedDataStorage targetStorage, + final @Nullable Dynamic levelData + ) { + targetStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(requireNonNullElseGet(readLegacyUuid(this.sourceRoot), UUID::randomUUID))); + final PaperWorldPDC preservedPdc = WorldMigrationSupport.readLegacyPdc(levelData, this.registryAccess); + if (preservedPdc != null) { + targetStorage.set(PaperWorldPDC.TYPE, preservedPdc); + } + targetStorage.set(PaperLevelOverrides.TYPE, PaperLevelOverrides.createFromRawLevelData(levelData)); + } + + private List explicitDataRoots(final List dimensionRoots) { + final LinkedHashSet dataRoots = new LinkedHashSet<>(); + if (dimensionRoots.contains(this.sourceRoot)) { + dataRoots.add(this.sourceRoot.resolve(net.minecraft.world.level.storage.LevelResource.DATA.id())); + } + for (final Path dimensionRoot : dimensionRoots) { + if (dimensionRoot.equals(this.sourceRoot)) { + continue; + } + dataRoots.add(dimensionRoot.resolve(net.minecraft.world.level.storage.LevelResource.DATA.id())); + } + return List.copyOf(dataRoots); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java b/paper-server/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java new file mode 100644 index 000000000000..53d45281db83 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/migration/VanillaWorldMigration.java @@ -0,0 +1,146 @@ +package io.papermc.paper.world.migration; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; +import io.papermc.paper.world.saveddata.PaperWorldPDC; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gamerules.GameRuleMap; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.saveddata.SavedDataType; +import net.minecraft.world.level.saveddata.WanderingTraderData; +import net.minecraft.world.level.saveddata.WeatherData; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.SavedDataStorage; +import net.minecraft.world.level.timers.TimerQueue; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +@NullMarked +final class VanillaWorldMigration { + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private VanillaWorldMigration() { + } + + static void migrate(final WorldMigrationContext context) throws IOException { + LOGGER.info("Starting Vanilla import for world '{}' ({})", context.worldName(), context.dimensionKey().identifier()); + final var levelDataResult = WorldMigrationSupport.readFixedLevelData(context.rootAccess()); + if (levelDataResult.fatalError()) { + throw new IOException("Failed to read level data for world migration"); + } + + final boolean rootOwnsThisWorld = context.dimensionKey() == Level.OVERWORLD; + + if (rootOwnsThisWorld) { + WorldMigrationSupport.migratePaperWorldConfig(context.baseRoot(), context.targetDimensionPath()); + } + + migrateSavedData(context); + createLevelOverrides(context, levelDataResult.dataTag()); + migrateLegacyPdc(context, levelDataResult.dataTag()); + + LOGGER.info("Completed Vanilla import for world '{}' ({})", context.worldName(), context.dimensionKey().identifier()); + } + + private static void migrateSavedData(final WorldMigrationContext context) throws IOException { + final List sourceDataRoots = List.of( + context.rootAccess().getDimensionPath(Level.OVERWORLD).resolve(LevelResource.DATA.id()), + context.baseRoot().resolve(LevelResource.DATA.id()) + ); + + WorldMigrationSupport.copySavedDataIfPresent(sourceDataRoots, context.targetDataRoot(), WorldGenSettings.TYPE, false); + WorldMigrationSupport.copySavedDataIfPresent(sourceDataRoots, context.targetDataRoot(), GameRuleMap.TYPE, false); + WorldMigrationSupport.copySavedDataIfPresent(sourceDataRoots, context.targetDataRoot(), WeatherData.TYPE, false); + if (context.dimensionKey() == Level.OVERWORLD) { + WorldMigrationSupport.copySavedDataIfPresent(sourceDataRoots, context.targetDataRoot(), TimerQueue.TYPE, false); + WorldMigrationSupport.copySavedDataIfPresent(sourceDataRoots, context.targetDataRoot(), WanderingTraderData.TYPE, false); + + deleteLegacyRootCopyIfMigrated(context, WorldGenSettings.TYPE); + deleteLegacyRootCopyIfMigrated(context, GameRuleMap.TYPE); + deleteLegacyRootCopyIfMigrated(context, WeatherData.TYPE); + deleteLegacyRootCopyIfMigrated(context, TimerQueue.TYPE); + deleteLegacyRootCopyIfMigrated(context, WanderingTraderData.TYPE); + } + } + + private static void deleteLegacyRootCopyIfMigrated(final WorldMigrationContext context, final SavedDataType type) throws IOException { + final Path target = WorldMigrationSupport.savedDataPath(context.targetDataRoot(), type); + if (!Files.isRegularFile(target)) { + return; + } + + final Path rootSource = WorldMigrationSupport.savedDataPath(context.baseRoot().resolve(LevelResource.DATA.id()), type); + Files.deleteIfExists(rootSource); + } + + private static void createLevelOverrides( + final WorldMigrationContext context, + final @Nullable Dynamic levelData + ) { + final Path levelOverridesPath = WorldMigrationSupport.savedDataPath(context.targetDataRoot(), PaperLevelOverrides.TYPE); + if (Files.exists(levelOverridesPath)) { + return; + } + + final PaperLevelOverrides levelOverrides = PaperLevelOverrides.createFromRawLevelData(levelData); + final ResourceKey explicitPaperRespawnDimension = resolveExplicitPaperRespawnDimension(levelData); + if (explicitPaperRespawnDimension == null && context.dimensionKey() != resolveVanillaRespawnDimension(levelData)) { + levelOverrides.setInitialized(false); + } + + final SavedDataStorage targetStorage = new SavedDataStorage(context.targetDataRoot(), DataFixers.getDataFixer(), context.registryAccess()); + targetStorage.set(PaperLevelOverrides.TYPE, levelOverrides); + targetStorage.saveAndJoin(); + } + + private static void migrateLegacyPdc( + final WorldMigrationContext context, + final @Nullable Dynamic levelData + ) { + final Path pdcPath = WorldMigrationSupport.savedDataPath(context.targetDataRoot(), PaperWorldPDC.TYPE); + if (!Files.exists(pdcPath)) { + final PaperWorldPDC pdc = WorldMigrationSupport.readLegacyPdc(levelData, context.registryAccess()); + if (pdc != null) { + final SavedDataStorage targetStorage = new SavedDataStorage(context.targetDataRoot(), DataFixers.getDataFixer(), context.registryAccess()); + targetStorage.set(PaperWorldPDC.TYPE, pdc); + targetStorage.saveAndJoin(); + + WorldMigrationSupport.clearLegacyPdc(levelData); + context.rootAccess().saveLevelData(levelData); + } + } + } + + private static @Nullable ResourceKey resolveExplicitPaperRespawnDimension(final @Nullable Dynamic levelData) { + if (levelData == null) { + return null; + } + + return levelData.get(PrimaryLevelData.PAPER_RESPAWN_DIMENSION) + .read(Level.RESOURCE_KEY_CODEC) + .result() + .orElse(null); + } + + private static ResourceKey resolveVanillaRespawnDimension(final @Nullable Dynamic levelData) { + if (levelData == null) { + return Level.OVERWORLD; + } + + return levelData.get("spawn") + .read(LevelData.RespawnData.CODEC) + .result() + .map(LevelData.RespawnData::dimension) + .orElse(Level.OVERWORLD); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/migration/WorldFolderMigration.java b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldFolderMigration.java new file mode 100644 index 000000000000..9b6da34e53e5 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldFolderMigration.java @@ -0,0 +1,102 @@ +package io.papermc.paper.world.migration; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; +import io.papermc.paper.world.saveddata.PaperWorldMetadata; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.jspecify.annotations.NullMarked; +import org.slf4j.Logger; + +@NullMarked +public final class WorldFolderMigration { + private static final Logger LOGGER = LogUtils.getClassLogger(); + private static final boolean DISABLE_MIGRATION_DELAY = Boolean.getBoolean("paper.disableMigrationDelay"); + private static boolean startupMigrationWarningShown; + + private WorldFolderMigration() { + } + + private enum MigrationMode { + LEGACY_CRAFTBUKKIT_MIGRATION, + VANILLA_MIGRATION, + NO_OP + } + + public static void migrateStartupWorld( + final LevelStorageSource.LevelStorageAccess rootAccess, + final HolderLookup.Provider registryAccess, + final String worldName, + final ResourceKey stemKey, + final ResourceKey dimensionKey + ) throws IOException { + final WorldMigrationContext context = new WorldMigrationContext(rootAccess, registryAccess, worldName, stemKey, dimensionKey); + final MigrationMode mode = classifyStartupMigration(context); + if (mode != MigrationMode.NO_OP) { + warnAndDelayStartupMigration(); + } + switch (mode) { + case LEGACY_CRAFTBUKKIT_MIGRATION -> LegacyCraftBukkitWorldMigration.migrate(context); + case VANILLA_MIGRATION -> VanillaWorldMigration.migrate(context); + case NO_OP -> {} + } + } + + public static void migrateApiWorld( + final LevelStorageSource.LevelStorageAccess rootAccess, + final HolderLookup.Provider registryAccess, + final String worldName, + final ResourceKey stemKey, + final ResourceKey dimensionKey + ) throws IOException { + LegacyCraftBukkitWorldMigration.migrateApiWorld(new WorldMigrationContext(rootAccess, registryAccess, worldName, stemKey, dimensionKey)); + } + + private static synchronized void warnAndDelayStartupMigration() throws IOException { + if (startupMigrationWarningShown) { + return; + } + startupMigrationWarningShown = true; + LOGGER.warn("===================== ! ALERT ! ====================="); + LOGGER.warn("World storage migration is required during startup."); + LOGGER.warn("If you do not have a backup: interrupt the server now. Use Ctrl+C, your panel kill function, etc."); + LOGGER.warn("====================================================="); + LOGGER.warn("Continuing in 30 seconds..."); + if (DISABLE_MIGRATION_DELAY) { + LOGGER.warn("Migration delay disabled by system property."); + } else { + try { + Thread.sleep(30_000L); + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting before startup world migration", ex); + } + } + LOGGER.info("Continuing with startup world migration."); + } + + private static MigrationMode classifyStartupMigration(final WorldMigrationContext context) { + if (!context.rootAccess().hasWorldData()) { + return MigrationMode.NO_OP; + } + if (!context.rootAccess().getLevelId().equals(context.worldName()) && Files.isDirectory(context.rootAccess().parent().getLevelPath(context.worldName()))) { + return MigrationMode.LEGACY_CRAFTBUKKIT_MIGRATION; + } + return hasCurrentPaperData(context.rootAccess(), context.dimensionKey()) ? MigrationMode.NO_OP : MigrationMode.VANILLA_MIGRATION; + } + + static boolean hasCurrentPaperData(final LevelStorageSource.LevelStorageAccess rootAccess, final ResourceKey dimensionKey) { + final Path targetDataRoot = rootAccess.getDimensionPath(dimensionKey).resolve(LevelResource.DATA.id()); + return Files.isRegularFile(WorldMigrationSupport.savedDataPath(targetDataRoot, PaperWorldMetadata.TYPE)) + && Files.isRegularFile(WorldMigrationSupport.savedDataPath(targetDataRoot, PaperLevelOverrides.TYPE)) + && Files.isRegularFile(WorldMigrationSupport.savedDataPath(targetDataRoot, WorldGenSettings.TYPE)); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationContext.java b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationContext.java new file mode 100644 index 000000000000..4e6a8b78f01d --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationContext.java @@ -0,0 +1,31 @@ +package io.papermc.paper.world.migration; + +import java.nio.file.Path; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.jspecify.annotations.NullMarked; + +@NullMarked +record WorldMigrationContext( + LevelStorageSource.LevelStorageAccess rootAccess, + HolderLookup.Provider registryAccess, + String worldName, + ResourceKey stemKey, + ResourceKey dimensionKey +) { + Path baseRoot() { + return this.rootAccess.levelDirectory.path(); + } + + Path targetDimensionPath() { + return this.rootAccess.getDimensionPath(this.dimensionKey); + } + + Path targetDataRoot() { + return this.targetDimensionPath().resolve(LevelResource.DATA.id()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java new file mode 100644 index 000000000000..f4384489d524 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/migration/WorldMigrationSupport.java @@ -0,0 +1,187 @@ +package io.papermc.paper.world.migration; + +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Dynamic; +import io.papermc.paper.world.saveddata.PaperWorldPDC; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.NbtException; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.ReportedNbtException; +import net.minecraft.resources.Identifier; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.worldupdate.UpgradeProgress; +import net.minecraft.world.level.saveddata.SavedDataType; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.LevelSummary; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +@NullMarked +final class WorldMigrationSupport { + private static final Logger LOGGER = LogUtils.getClassLogger(); + static final List DIMENSION_DIRECTORIES = List.of("region", "entities", "poi"); + static final String PAPER_WORLD_CONFIG = "paper-world.yml"; + + private WorldMigrationSupport() { + } + + static @Nullable PaperWorldPDC readLegacyPdc(final @Nullable Dynamic levelData, final HolderLookup.Provider registryAccess) { + if (levelData == null) { + return null; + } + + return levelData.get("BukkitValues") + .result() + .flatMap(dynamic -> PaperWorldPDC.CODEC.parse(registryAccess.createSerializationContext(NbtOps.INSTANCE), dynamic.convert(NbtOps.INSTANCE).getValue()).result()) + .orElse(null); + } + + static void clearLegacyPdc(final Dynamic levelData) { + levelData.remove("BukkitValues"); + } + + static Path getStorageFolder(final Identifier dimension, final Path baseFolder) { + return dimension.resolveAgainst(baseFolder.resolve("dimensions")); + } + + static void migratePaperWorldConfig(final Path sourceRoot, final Path targetDimensionPath) throws IOException { + final Path source = sourceRoot.resolve(PAPER_WORLD_CONFIG); + if (!Files.isRegularFile(source)) { + return; + } + + final Path target = targetDimensionPath.resolve(PAPER_WORLD_CONFIG); + if (Files.exists(target)) { + return; + } + + Files.createDirectories(target.getParent()); + LOGGER.info("Migrating Paper world config from {} to {}", source, target); + Files.move(source, target); + } + + static void migrateDimensionDirectories(final Path sourceDimensionRoot, final Path targetDimensionPath) throws IOException { + if (sourceDimensionRoot.equals(targetDimensionPath)) { + return; + } + + for (final String directory : DIMENSION_DIRECTORIES) { + final Path source = sourceDimensionRoot.resolve(directory); + if (!Files.exists(source)) { + continue; + } + + final Path target = targetDimensionPath.resolve(directory); + LOGGER.info("Migrating world directory from {} to {}", source, target); + mergeMove(source, target); + } + } + + private static void mergeMove(final Path source, final Path target) throws IOException { + if (Files.isDirectory(source)) { + Files.createDirectories(target); + try (final var entries = Files.list(source)) { + for (final Path child : entries.toList()) { + mergeMove(child, target.resolve(child.getFileName().toString())); + } + } + tryDeleteIfEmpty(source); + return; + } + + if (Files.exists(target)) { + throw new IOException("Refusing to overwrite existing migrated file " + target + " while moving " + source); + } + + Files.createDirectories(target.getParent()); + Files.move(source, target); + } + + private static void tryDeleteIfEmpty(final Path path) throws IOException { + try (final var entries = Files.list(path)) { + if (entries.findAny().isPresent()) { + return; + } + } + Files.deleteIfExists(path); + } + + record LevelDataResult(@Nullable Dynamic dataTag, boolean fatalError) {} + + static LevelDataResult readFixedLevelData(final LevelStorageSource.LevelStorageAccess access) { + if (!access.hasWorldData()) { + return new LevelDataResult(null, false); + } + + final Dynamic levelDataUnfixed; + try { + levelDataUnfixed = access.getUnfixedDataTagWithFallback(); + } catch (final NbtException | ReportedNbtException | IOException ex) { + LOGGER.error("Failed to load world data. World files may be corrupted. Shutting down.", ex); + return new LevelDataResult(null, true); + } + + final LevelSummary summary = access.fixAndGetSummaryFromTag(levelDataUnfixed); + if (summary.requiresManualConversion()) { + LOGGER.info("This world must be opened in an older version (like 1.6.4) to be safely converted"); + return new LevelDataResult(null, true); + } + + if (!summary.isCompatible()) { + LOGGER.info("This world was created by an incompatible version."); + return new LevelDataResult(null, true); + } + + return new LevelDataResult(DataFixers.getFileFixer().fix(access, levelDataUnfixed, new UpgradeProgress()), false); + } + + static Path savedDataPath(final Path dataRoot, final SavedDataType type) { + return type.id().withSuffix(".dat").resolveAgainst(dataRoot); + } + + static boolean copySavedDataIfPresent( + final List sourceDataRoots, + final Path targetDataRoot, + final SavedDataType type, + final boolean overwrite + ) throws IOException { + return copySavedDataFileIfPresent(targetDataRoot, findSavedDataFile(sourceDataRoots, type), type, overwrite); + } + + static boolean copySavedDataFileIfPresent( + final Path targetDataRoot, + final @Nullable Path source, + final SavedDataType type, + final boolean overwrite + ) throws IOException { + if (source == null) { + return false; + } + + final Path target = savedDataPath(targetDataRoot, type); + if (!overwrite && Files.isRegularFile(target)) { + return true; + } + + Files.createDirectories(target.getParent()); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + return true; + } + + static @Nullable Path findSavedDataFile(final List dataRoots, final SavedDataType type) { + for (final Path dataRoot : dataRoots) { + final Path source = savedDataPath(dataRoot, type); + if (Files.isRegularFile(source)) { + return source; + } + } + return null; + } + +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperLevelOverrides.java b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperLevelOverrides.java new file mode 100644 index 000000000000..b0bed5d36339 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperLevelOverrides.java @@ -0,0 +1,245 @@ +package io.papermc.paper.world.saveddata; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.Difficulty; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelSettings; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.ServerLevelData; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class PaperLevelOverrides extends SavedData implements ServerLevelData { + private static final Codec LEGACY_GAME_TYPE_CODEC = Codec.INT.xmap(GameType::byId, GameType::getId); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + RespawnData.CODEC.fieldOf("spawn").forGetter(levelOverrides -> levelOverrides.respawnData), + Codec.LONG.fieldOf("game_time").forGetter(levelOverrides -> levelOverrides.gameTime), + Codec.BOOL.fieldOf("initialized").forGetter(levelOverrides -> levelOverrides.initialized), + LEGACY_GAME_TYPE_CODEC.fieldOf("game_type").forGetter(levelOverrides -> levelOverrides.gameType), + LevelSettings.DifficultySettings.CODEC.fieldOf("difficulty_settings").forGetter(levelOverrides -> levelOverrides.difficultySettings) + ).apply(instance, PaperLevelOverrides::new)); + public static final SavedDataType TYPE = new SavedDataType<>( + Identifier.fromNamespaceAndPath(Identifier.PAPER_NAMESPACE, "level_overrides"), + PaperLevelOverrides::new, + CODEC, + DataFixTypes.NONE + ); + + private RespawnData respawnData; + private long gameTime; + private boolean initialized; + private GameType gameType; + private LevelSettings.DifficultySettings difficultySettings; + private transient @Nullable PrimaryLevelData rootData; + private transient ResourceKey dimension = Level.OVERWORLD; + + private PaperLevelOverrides() { + this(RespawnData.DEFAULT, 0L, false, GameType.SURVIVAL, LevelSettings.DifficultySettings.DEFAULT); + } + + private PaperLevelOverrides( + final RespawnData respawnData, + final long gameTime, + final boolean initialized, + final GameType gameType, + final LevelSettings.DifficultySettings difficultySettings + ) { + this.respawnData = respawnData.normalized(); + this.gameTime = gameTime; + this.initialized = initialized; + this.gameType = gameType; + this.difficultySettings = difficultySettings; + } + + public static PaperLevelOverrides createFromLiveLevelData(final PrimaryLevelData rootData) { + return new PaperLevelOverrides( + RespawnData.fromVanilla(rootData.getRespawnData()), + rootData.getGameTime(), + false, + rootData.getGameType(), + new LevelSettings.DifficultySettings(rootData.getDifficulty(), rootData.isHardcore(), rootData.isDifficultyLocked()) + ); + } + + public static PaperLevelOverrides createFromRawLevelData(final @Nullable Dynamic levelData) { + if (levelData == null) { + return new PaperLevelOverrides(); + } + return new PaperLevelOverrides( + RespawnData.fromVanilla(levelData.get("spawn").read(LevelData.RespawnData.CODEC).result().orElse(LevelData.RespawnData.DEFAULT)), + levelData.get("Time").asLong(0L), + levelData.get("initialized").asBoolean(true), + GameType.byId(levelData.get("GameType").asInt(GameType.SURVIVAL.getId())), + levelData.get("difficulty_settings").read(LevelSettings.DifficultySettings.CODEC).result().orElse(LevelSettings.DifficultySettings.DEFAULT) + ); + } + + public PaperLevelOverrides attach(final PrimaryLevelData rootData, final ResourceKey dimension) { + this.rootData = rootData; + this.dimension = dimension; + return this; + } + + private void setRespawn(final RespawnData respawnData) { + final RespawnData normalizedRespawnData = respawnData.normalized(); + if (!this.respawnData.equals(normalizedRespawnData)) { + this.respawnData = normalizedRespawnData; + // Do not syncRootData here, handled separately in MinecraftServer#updateEffectiveRespawnData + this.setDirty(); + } + } + + public void setDifficulty(final Difficulty difficulty) { + if (this.difficultySettings.difficulty() != difficulty) { + this.setDifficultySettings(new LevelSettings.DifficultySettings(difficulty, this.difficultySettings.hardcore(), this.difficultySettings.locked())); + } + } + + public void setHardcore(final boolean hardcore) { + if (this.difficultySettings.hardcore() != hardcore) { + this.setDifficultySettings(new LevelSettings.DifficultySettings(this.difficultySettings.difficulty(), hardcore, this.difficultySettings.locked())); + } + } + + public void setDifficultyLocked(final boolean difficultyLocked) { + if (this.difficultySettings.locked() != difficultyLocked) { + this.setDifficultySettings(new LevelSettings.DifficultySettings(this.difficultySettings.difficulty(), this.difficultySettings.hardcore(), difficultyLocked)); + } + } + + @Override + public LevelData.RespawnData getRespawnData() { + return this.respawnData.toVanilla(this.dimension); + } + + @Override + public long getGameTime() { + return this.gameTime; + } + + @Override + public GameType getGameType() { + return this.gameType; + } + + @Override + public String getLevelName() { + return this.rootDataOrThrow().getLevelName(); + } + + @Override + public void setGameTime(final long time) { + if (this.gameTime != time) { + this.gameTime = time; + this.syncRootData(rootData -> rootData.setGameTime(time)); + this.setDirty(); + } + } + + @Override + public void setSpawn(final LevelData.RespawnData respawnData) { + this.setRespawn(RespawnData.fromVanilla(respawnData)); + } + + @Override + public void setGameType(final GameType gameType) { + if (this.gameType != gameType) { + this.gameType = gameType; + this.syncRootData(rootData -> rootData.setGameType(gameType)); + this.setDirty(); + } + } + + @Override + public boolean isHardcore() { + return this.difficultySettings.hardcore(); + } + + @Override + public boolean isAllowCommands() { + return this.rootDataOrThrow().isAllowCommands(); + } + + @Override + public boolean isInitialized() { + return this.initialized; + } + + @Override + public void setInitialized(final boolean initialized) { + if (this.initialized != initialized) { + this.initialized = initialized; + this.syncRootData(rootData -> rootData.setInitialized(initialized)); + this.setDirty(); + } + } + + @Override + public Difficulty getDifficulty() { + return this.difficultySettings.difficulty(); + } + + @Override + public boolean isDifficultyLocked() { + return this.difficultySettings.locked(); + } + + private PrimaryLevelData rootDataOrThrow() { + if (this.rootData == null) { + throw new IllegalStateException("PaperWorldLevelOverrides not attached"); + } + return this.rootData; + } + + private void setDifficultySettings(final LevelSettings.DifficultySettings difficultySettings) { + if (!this.difficultySettings.equals(difficultySettings)) { + this.difficultySettings = difficultySettings; + this.syncRootData(rootData -> rootData.settings = rootData.settings + .withDifficulty(difficultySettings.difficulty()) + .withHardcore(difficultySettings.hardcore()) + .withDifficultyLock(difficultySettings.locked())); + this.setDirty(); + } + } + + private void syncRootData(final java.util.function.Consumer action) { + if (this.dimension == Level.OVERWORLD && this.rootData != null) { + action.accept(this.rootData); + } + } + + // Like LevelData.RespawnData, but with a BlockPos instead of a GlobalPos + public record RespawnData(BlockPos pos, float yaw, float pitch) { + public static final RespawnData DEFAULT = new RespawnData(BlockPos.ZERO, 0.0F, 0.0F); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + BlockPos.CODEC.fieldOf("pos").forGetter(RespawnData::pos), + Codec.floatRange(-180.0F, 180.0F).fieldOf("yaw").forGetter(RespawnData::yaw), + Codec.floatRange(-90.0F, 90.0F).fieldOf("pitch").forGetter(RespawnData::pitch) + ).apply(instance, RespawnData::new)); + + public RespawnData normalized() { + return new RespawnData(this.pos.immutable(), Mth.wrapDegrees(this.yaw), Mth.clamp(this.pitch, -90.0F, 90.0F)); + } + + public LevelData.RespawnData toVanilla(final ResourceKey dimension) { + final RespawnData normalized = this.normalized(); + return LevelData.RespawnData.of(dimension, normalized.pos, normalized.yaw, normalized.pitch); + } + + public static RespawnData fromVanilla(final LevelData.RespawnData respawnData) { + return new RespawnData(respawnData.pos(), respawnData.yaw(), respawnData.pitch()).normalized(); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldMetadata.java b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldMetadata.java new file mode 100644 index 000000000000..a90f216d4647 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldMetadata.java @@ -0,0 +1,42 @@ +package io.papermc.paper.world.saveddata; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Objects; +import java.util.UUID; +import net.minecraft.core.UUIDUtil; +import net.minecraft.resources.Identifier; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class PaperWorldMetadata extends SavedData { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + UUIDUtil.CODEC.fieldOf("uuid").forGetter(PaperWorldMetadata::uuid) + ).apply(instance, PaperWorldMetadata::new)); + public static final SavedDataType TYPE = new SavedDataType<>( + Identifier.fromNamespaceAndPath(Identifier.PAPER_NAMESPACE, "metadata"), + () -> new PaperWorldMetadata(UUID.randomUUID()), + CODEC, + DataFixTypes.NONE + ); + + private UUID uuid; + + public PaperWorldMetadata(final UUID uuid) { + this.uuid = uuid; + } + + public UUID uuid() { + return this.uuid; + } + + public void set(final UUID uuid) { + if (!Objects.equals(this.uuid, uuid)) { + this.uuid = uuid; + this.setDirty(); + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldPDC.java b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldPDC.java new file mode 100644 index 000000000000..9a38a0824f50 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/world/saveddata/PaperWorldPDC.java @@ -0,0 +1,42 @@ +package io.papermc.paper.world.saveddata; + +import com.mojang.serialization.Codec; +import java.util.Objects; +import net.minecraft.resources.Identifier; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; +import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer; +import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class PaperWorldPDC extends SavedData { + private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry(); + public static final Codec CODEC = CraftPersistentDataContainer.createCodec(DATA_TYPE_REGISTRY) + .xmap(PaperWorldPDC::new, PaperWorldPDC::persistentData); + public static final SavedDataType TYPE = new SavedDataType<>( + Identifier.fromNamespaceAndPath(Identifier.PAPER_NAMESPACE, "persistent_data_container"), + () -> new PaperWorldPDC(new CraftPersistentDataContainer(DATA_TYPE_REGISTRY)), + CODEC, + DataFixTypes.NONE + ); + + private final CraftPersistentDataContainer persistentData; + + public PaperWorldPDC(final CraftPersistentDataContainer persistentData) { + this.persistentData = persistentData; + } + + public CraftPersistentDataContainer persistentData() { + return this.persistentData; + } + + public void setFrom(final CraftPersistentDataContainer source) { + if (!Objects.equals(this.persistentData, source)) { + this.persistentData.clear(); + this.persistentData.putAll(source.getTagsCloned()); + this.setDirty(); + } + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java index c7fef2e9b9d6..e892844dae4b 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -10,12 +10,12 @@ import com.google.common.collect.MapMaker; import com.mojang.brigadier.StringReader; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.serialization.Dynamic; -import com.mojang.serialization.Lifecycle; import io.papermc.paper.configuration.GlobalConfiguration; import io.papermc.paper.configuration.PaperServerConfiguration; import io.papermc.paper.configuration.ServerConfiguration; import io.papermc.paper.world.PaperWorldLoader; +import io.papermc.paper.world.migration.WorldFolderMigration; +import io.papermc.paper.world.saveddata.PaperLevelOverrides; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -26,6 +26,7 @@ import java.io.InputStreamReader; import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -44,6 +45,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import javax.imageio.ImageIO; +import net.md_5.bungee.api.chat.BaseComponent; import net.minecraft.Optionull; import net.minecraft.advancements.AdvancementHolder; import net.minecraft.commands.CommandSourceStack; @@ -53,8 +55,8 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; -import net.minecraft.resources.ResourceKey; import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ReloadableServerRegistries; import net.minecraft.server.WorldLoader; @@ -76,7 +78,6 @@ import net.minecraft.tags.TagKey; import net.minecraft.util.GsonHelper; import net.minecraft.util.datafix.DataFixers; -import net.minecraft.world.Difficulty; import net.minecraft.world.damagesource.DamageType; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ai.village.VillageSiege; @@ -96,7 +97,6 @@ import net.minecraft.world.item.crafting.RepairItemRecipe; import net.minecraft.world.level.CustomSpawner; import net.minecraft.world.level.GameType; -import net.minecraft.world.level.LevelSettings; import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.dimension.LevelStem; @@ -108,13 +108,11 @@ import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.saveddata.maps.MapId; import net.minecraft.world.level.saveddata.maps.MapItemSavedData; -import net.minecraft.world.level.storage.LevelDataAndDimensions; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.PlayerDataStorage; import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.level.storage.SavedDataStorage; -import net.minecraft.world.level.validation.ContentValidationException; import org.apache.commons.lang3.StringUtils; import org.bukkit.BanList; import org.bukkit.Bukkit; @@ -262,8 +260,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.error.MarkedYAMLException; -import net.md_5.bungee.api.chat.BaseComponent; // Spigot - public final class CraftServer implements Server { private final String serverName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName(); private final String serverVersion; @@ -1181,10 +1177,8 @@ public World createWorld(WorldCreator creator) { String name = creator.name(); ChunkGenerator chunkGenerator = creator.generator(); BiomeProvider biomeProvider = creator.biomeProvider(); - File folder = new File(this.getWorldContainer(), name); - World world = this.getWorld(name); - // Paper start + World world = this.getWorld(name); World worldByKey = this.getWorld(creator.key()); if (world != null || worldByKey != null) { if (world == worldByKey) { @@ -1192,11 +1186,6 @@ public World createWorld(WorldCreator creator) { } throw new IllegalArgumentException("Cannot create a world with key " + creator.key() + " and name " + name + " one (or both) already match a world that exists"); } - // Paper end - - if (folder.exists()) { - Preconditions.checkArgument(folder.isDirectory(), "File (%s) exists and isn't a folder", name); - } if (chunkGenerator == null) { chunkGenerator = this.getGenerator(name); @@ -1213,80 +1202,79 @@ public World createWorld(WorldCreator creator) { default -> throw new IllegalArgumentException("Illegal dimension (" + creator.environment() + ")"); }; - LevelStorageSource.LevelStorageAccess levelStorageAccess; - try { - levelStorageAccess = LevelStorageSource.createDefault(this.getWorldContainer().toPath()).validateAndCreateAccess(name, actualDimension); - } catch (IOException | ContentValidationException ex) { - throw new RuntimeException(ex); - } - - boolean hardcore = creator.hardcore(); - - LevelDataAndDimensions.WorldDataAndGenSettings worldDataAndGenSettings; + final ResourceKey dimensionKey = PaperWorldLoader.dimensionKey(creator.key()); WorldLoader.DataLoadContext context = this.console.worldLoaderContext; RegistryAccess.Frozen registryAccess = context.datapackDimensions(); net.minecraft.core.Registry contextLevelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM); - Dynamic dataTag = PaperWorldLoader.getLevelData(levelStorageAccess).dataTag(); - if (dataTag != null) { - LevelDataAndDimensions levelDataAndDimensions = LevelStorageSource.getLevelDataAndDimensions( - levelStorageAccess, dataTag, context.dataConfiguration(), contextLevelStemRegistry, context.datapackWorldgen() + final LevelStem configuredStem = this.console.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getValue(actualDimension); + if (configuredStem == null) { + throw new IllegalStateException("Missing configured level stem " + actualDimension); + } + try { + WorldFolderMigration.migrateApiWorld( + this.console.storageSource, + this.console.registryAccess(), + name, + actualDimension, + dimensionKey ); - worldDataAndGenSettings = levelDataAndDimensions.worldDataAndGenSettings(); - registryAccess = levelDataAndDimensions.dimensions().dimensionsRegistryAccess(); - } else { - LevelSettings levelSettings; + } catch (final IOException ex) { + throw new RuntimeException("Failed to migrate legacy world " + name, ex); + } + PaperWorldLoader.LoadedWorldData loadedWorldData = PaperWorldLoader.loadWorldData( + this.console, + dimensionKey, + name + ); + final PrimaryLevelData primaryLevelData = (PrimaryLevelData) this.console.getWorldData(); + WorldGenSettings worldGenSettings = LevelStorageSource.readExistingSavedData(this.console.storageSource, dimensionKey, this.console.registryAccess(), WorldGenSettings.TYPE) + .result() + .orElse(null); + if (worldGenSettings == null) { WorldOptions worldOptions = new WorldOptions(creator.seed(), creator.generateStructures(), creator.bonusChest()); - WorldDimensions worldDimensions; DedicatedServerProperties.WorldDimensionData properties = new DedicatedServerProperties.WorldDimensionData(GsonHelper.parse((creator.generatorSettings().isEmpty()) ? "{}" : creator.generatorSettings()), creator.type().name().toLowerCase(Locale.ROOT)); - levelSettings = new LevelSettings( - name, - GameType.byId(this.getDefaultGameMode().getValue()), - new LevelSettings.DifficultySettings(Difficulty.EASY, hardcore, false), - false, - context.dataConfiguration() - ); - worldDimensions = properties.create(context.datapackWorldgen()); + WorldDimensions worldDimensions = properties.create(context.datapackWorldgen()); WorldDimensions.Complete complete = worldDimensions.bake(contextLevelStemRegistry); - Lifecycle lifecycle = complete.lifecycle().add(context.datapackWorldgen().allRegistriesLifecycle()); + if (complete.dimensions().getValue(actualDimension) == null) { + throw new IllegalStateException("Missing generated level stem " + actualDimension + " for world " + name); + } - worldDataAndGenSettings = new LevelDataAndDimensions.WorldDataAndGenSettings( - new PrimaryLevelData(levelSettings, complete.specialWorldProperty(), lifecycle), - new WorldGenSettings(worldOptions, worldDimensions) - ); + worldGenSettings = new WorldGenSettings(worldOptions, worldDimensions); registryAccess = complete.dimensionsRegistryAccess(); + loadedWorldData.levelOverrides().setHardcore(creator.hardcore()); + loadedWorldData = new PaperWorldLoader.LoadedWorldData( + loadedWorldData.bukkitName(), + loadedWorldData.uuid(), + loadedWorldData.pdc(), + loadedWorldData.levelOverrides() + ); } - final PrimaryLevelData primaryLevelData = (PrimaryLevelData) worldDataAndGenSettings.data(); + final WorldGenSettings genSettingsFinal = worldGenSettings; contextLevelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM); - primaryLevelData.checkName(name); - primaryLevelData.setModdedInfo(this.console.getServerModName(), this.console.getModdedStatus().shouldReportAsModified()); if (this.console.options.has("forceUpgrade")) { - net.minecraft.server.Main.forceUpgrade(levelStorageAccess, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, registryAccess, this.console.options.has("recreateRegionFiles")); + net.minecraft.server.Main.forceUpgrade(this.console.storageSource, DataFixers.getDataFixer(), this.console.options.has("eraseCache"), () -> true, registryAccess, this.console.options.has("recreateRegionFiles")); } - long biomeZoomSeed = BiomeManager.obfuscateSeed(worldDataAndGenSettings.genSettings().options().seed()); - LevelStem customStem = contextLevelStemRegistry.getValue(actualDimension); + long biomeZoomSeed = BiomeManager.obfuscateSeed(genSettingsFinal.options().seed()); + LevelStem customStem = genSettingsFinal.dimensions().get(actualDimension).orElse(null); + if (customStem == null) { + customStem = contextLevelStemRegistry.getValue(actualDimension); + } + if (customStem == null) { + throw new IllegalStateException("Missing level stem for world " + name + " using key " + actualDimension); + } - WorldInfo worldInfo = new CraftWorldInfo(worldDataAndGenSettings, levelStorageAccess, creator.environment(), customStem.type().value(), customStem.generator(), this.getHandle().getServer().registryAccess()); // Paper - Expose vanilla BiomeProvider from WorldInfo + WorldInfo worldInfo = new CraftWorldInfo(loadedWorldData.bukkitName(), genSettingsFinal.options().seed(), primaryLevelData.enabledFeatures(), creator.environment(), customStem.type().value(), customStem.generator(), this.getHandle().getServer().registryAccess(), loadedWorldData.uuid()); if (biomeProvider == null && chunkGenerator != null) { biomeProvider = chunkGenerator.getDefaultBiomeProvider(worldInfo); } - ResourceKey dimensionKey; - String levelName = this.getServer().getProperties().levelName; - if (name.equals(levelName + "_nether")) { - dimensionKey = net.minecraft.world.level.Level.NETHER; - } else if (name.equals(levelName + "_the_end")) { - dimensionKey = net.minecraft.world.level.Level.END; - } else { - dimensionKey = ResourceKey.create(Registries.DIMENSION, Identifier.fromNamespaceAndPath(creator.key().namespace(), creator.key().value())); - } - - final SavedDataStorage savedDataStorage = new SavedDataStorage(levelStorageAccess.getLevelPath(LevelResource.DATA), this.console.getFixerUpper(), this.console.registryAccess()); - savedDataStorage.set(WorldGenSettings.TYPE, worldDataAndGenSettings.genSettings()); + final SavedDataStorage savedDataStorage = new SavedDataStorage(this.console.storageSource.getDimensionPath(dimensionKey).resolve(LevelResource.DATA.id()), this.console.getFixerUpper(), this.console.registryAccess()); + savedDataStorage.set(WorldGenSettings.TYPE, new WorldGenSettings(genSettingsFinal.options(), genSettingsFinal.dimensions())); List list = ImmutableList.of( new PhantomSpawner(), new PatrolSpawner(), new CatSpawner(), new VillageSiege(), new WanderingTraderSpawner(savedDataStorage) ); @@ -1294,29 +1282,30 @@ public World createWorld(WorldCreator creator) { ServerLevel serverLevel = new ServerLevel( this.console, this.console.executor, - levelStorageAccess, - worldDataAndGenSettings, + this.console.storageSource, + genSettingsFinal, dimensionKey, customStem, primaryLevelData.isDebugWorld(), biomeZoomSeed, creator.environment() == Environment.NORMAL ? list : ImmutableList.of(), true, + actualDimension, creator.environment(), chunkGenerator, biomeProvider, - savedDataStorage + savedDataStorage, + loadedWorldData ); if (!(this.worlds.containsKey(name.toLowerCase(Locale.ROOT)))) { return null; } - this.console.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up + this.console.addLevel(serverLevel); this.console.initWorld(serverLevel); serverLevel.setSpawnSettings(true); - // Paper - Put world into worldlist before initing the world; move up this.getServer().prepareLevel(serverLevel); @@ -1361,7 +1350,6 @@ public boolean unloadWorld(World world, boolean save) { handle.getChunkSource().close(save); io.papermc.paper.FeatureHooks.closeEntityManager(handle, save); // SPIGOT-6722: close entityManager // Paper - chunk system - handle.levelStorageAccess.close(); } catch (Exception ex) { this.getLogger().log(Level.SEVERE, null, ex); } @@ -1380,7 +1368,7 @@ public World getRespawnWorld() { public void setRespawnWorld(final World world) { Preconditions.checkArgument(world != null, "world cannot be null"); - this.console.overworld().serverLevelData.respawnDimension = ((CraftWorld) world).getHandle().dimension(); + ((PrimaryLevelData) this.console.getWorldData()).respawnDimension = ((CraftWorld) world).getHandle().dimension(); this.console.updateEffectiveRespawnData(); } @@ -1415,7 +1403,7 @@ public World getWorld(net.kyori.adventure.key.Key worldKey) { public void addWorld(World world) { // Check if a World already exists with the UID. if (this.getWorld(world.getUID()) != null) { - System.out.println("World " + world.getName() + " is a duplicate of another world and has been prevented from loading. Please delete the uid.dat file from " + world.getName() + "'s world directory if you want to be able to load the duplicate world."); + System.out.println("World " + world.getName() + " is a duplicate of another world and has been prevented from loading. Please remove or change the duplicated Paper world metadata before loading it again."); return; } this.worlds.put(world.getName().toLowerCase(Locale.ROOT), world); @@ -1841,7 +1829,7 @@ public CraftMapView createMap(World world) { ServerLevel level = ((CraftWorld) world).getHandle(); // creates a new map at world spawn with the scale of 3, without tracking position and unlimited tracking - BlockPos spawn = level.getLevelData().getRespawnData().pos(); + BlockPos spawn = level.serverLevelData.getRespawnData().pos(); MapId newId = MapItem.createNewSavedData(level, spawn.getX(), spawn.getZ(), 3, false, false, level.dimension()); return level.getMapData(newId).mapView; } @@ -2186,6 +2174,11 @@ public File getWorldContainer() { return this.getServer().storageSource.getLevelDirectory().path().getParent().toFile(); } + @Override + public Path getLevelDirectory() { + return this.getServer().storageSource.getLevelDirectory().path(); + } + @Override public OfflinePlayer[] getOfflinePlayers() { PlayerDataStorage storage = this.console.playerDataStorage; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 77b73a6c54c5..0a3c290edcf6 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -76,7 +76,6 @@ import net.minecraft.world.level.gamerules.GameRule; import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.storage.LevelData; -import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; @@ -325,7 +324,7 @@ public Block getBlockAt(int x, int y, int z) { @Override public Location getSpawnLocation() { - final LevelData.RespawnData respawnData = this.world.serverLevelData.getRespawnData(); + final var respawnData = this.world.serverLevelData.getRespawnData(); return CraftLocation.toBukkit(respawnData.pos(), this, respawnData.yaw(), respawnData.pitch()); } @@ -338,11 +337,7 @@ public boolean setSpawnLocation(Location location) { private boolean setSpawnLocation(int x, int y, int z, float yaw, float pitch) { try { - Location previousLocation = this.getSpawnLocation(); - this.world.serverLevelData.setSpawn(LevelData.RespawnData.of(this.world.dimension(), new BlockPos(x, y, z), yaw, pitch)); - - this.server.getServer().updateEffectiveRespawnData(); - new SpawnChangeEvent(this, previousLocation).callEvent(); + this.world.setRespawnData(LevelData.RespawnData.of(this.world.dimension(), new BlockPos(x, y, z), yaw, pitch)); return true; } catch (Exception e) { return false; @@ -764,7 +759,7 @@ public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate del @Override public String getName() { - return this.world.serverLevelData.getLevelName(); + return this.world.bukkitName; } @Override @@ -1427,7 +1422,7 @@ public boolean equals(Object obj) { @Override public Path getWorldPath() { - return this.world.levelStorageAccess.getLevelPath(LevelResource.ROOT).getParent(); + return this.world.getServer().storageSource.getDimensionPath(this.world.dimension()); } @Override @@ -1457,12 +1452,12 @@ public org.bukkit.WorldType getWorldType() { @Override public boolean canGenerateStructures() { - return this.world.worldDataAndGenSettings.genSettings().options().generateStructures(); + return this.world.worldGenSettings.options().generateStructures(); } @Override public boolean hasBonusChest() { - return this.world.worldDataAndGenSettings.genSettings().options().generateBonusChest(); + return this.world.worldGenSettings.options().generateBonusChest(); } @Override @@ -1472,7 +1467,7 @@ public boolean isHardcore() { @Override public void setHardcore(boolean hardcore) { - this.world.serverLevelData.settings = this.world.serverLevelData.settings.withHardcore(hardcore); + this.world.serverLevelData.setHardcore(hardcore); } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CraftWorldInfo.java b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CraftWorldInfo.java index f454fe6fe317..cf3d785013a2 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CraftWorldInfo.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CraftWorldInfo.java @@ -1,12 +1,13 @@ package org.bukkit.craftbukkit.generator; import java.util.UUID; +import net.minecraft.core.RegistryAccess; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.storage.LevelDataAndDimensions; -import net.minecraft.world.level.storage.LevelStorageSource; import org.bukkit.World; import org.bukkit.craftbukkit.block.CraftBiome; -import org.bukkit.craftbukkit.util.WorldUUID; import org.bukkit.generator.WorldInfo; public class CraftWorldInfo implements WorldInfo { @@ -17,20 +18,29 @@ public class CraftWorldInfo implements WorldInfo { private final long seed; private final int minHeight; private final int maxHeight; - private final net.minecraft.world.flag.FeatureFlagSet enabledFeatures; - private final net.minecraft.world.level.chunk.ChunkGenerator vanillaChunkGenerator; - private final net.minecraft.core.RegistryAccess.Frozen registryAccess; + private final FeatureFlagSet enabledFeatures; + private final ChunkGenerator vanillaChunkGenerator; + private final RegistryAccess.Frozen registryAccess; - public CraftWorldInfo(LevelDataAndDimensions.WorldDataAndGenSettings settings, LevelStorageSource.LevelStorageAccess session, World.Environment environment, DimensionType dimensionManager, net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator, net.minecraft.core.RegistryAccess.Frozen registryAccess) { - this.registryAccess = registryAccess; - this.vanillaChunkGenerator = chunkGenerator; - this.name = settings.data().getLevelName(); - this.uuid = WorldUUID.getOrCreate(session.levelDirectory.path().toFile()); + public CraftWorldInfo( + String name, + long seed, + FeatureFlagSet enabledFeatures, + World.Environment environment, + DimensionType dimensionManager, + ChunkGenerator vanillaChunkGenerator, + RegistryAccess.Frozen registryAccess, + UUID uuid + ) { + this.name = name; + this.seed = seed; + this.enabledFeatures = enabledFeatures; this.environment = environment; - this.seed = settings.genSettings().options().seed(); this.minHeight = dimensionManager.minY(); this.maxHeight = dimensionManager.minY() + dimensionManager.height(); - this.enabledFeatures = settings.data().enabledFeatures(); + this.vanillaChunkGenerator = vanillaChunkGenerator; + this.registryAccess = registryAccess; + this.uuid = uuid; } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/util/WorldUUID.java b/paper-server/src/main/java/org/bukkit/craftbukkit/util/WorldUUID.java deleted file mode 100644 index fd37759e6b1e..000000000000 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/util/WorldUUID.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.bukkit.craftbukkit.util; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.UUID; -import com.mojang.logging.LogUtils; -import org.slf4j.Logger; - -public final class WorldUUID { - - private static final Logger LOGGER = LogUtils.getLogger(); - - private WorldUUID() { - } - - public static UUID getOrCreate(File worldDir) { - File fileId = new File(worldDir, "uid.dat"); - if (fileId.exists()) { - try (DataInputStream inputStream = new DataInputStream(new FileInputStream(fileId))) { - return new UUID(inputStream.readLong(), inputStream.readLong()); - } catch (IOException ex) { - LOGGER.warn("Failed to read {}, generating new random UUID", fileId, ex); - } - } - - UUID uuid = UUID.randomUUID(); - try (DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(fileId))) { - outputStream.writeLong(uuid.getMostSignificantBits()); - outputStream.writeLong(uuid.getLeastSignificantBits()); - } catch (IOException ex) { - LOGGER.warn("Failed to write {}", fileId, ex); - } - return uuid; - } -}