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 super net.kyori.adventure.text.Component> 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;
- }
-}