diff --git a/paper-api/src/main/java/org/bukkit/WorldCreator.java b/paper-api/src/main/java/org/bukkit/WorldCreator.java index 4b312399eced..5519513a480f 100644 --- a/paper-api/src/main/java/org/bukkit/WorldCreator.java +++ b/paper-api/src/main/java/org/bukkit/WorldCreator.java @@ -1,7 +1,9 @@ package org.bukkit; import com.google.common.base.Preconditions; +import java.nio.file.Path; import java.util.Random; +import io.papermc.paper.math.Position; import org.bukkit.command.CommandSender; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.ChunkGenerator; @@ -25,6 +27,15 @@ public class WorldCreator { private boolean hardcore = false; private boolean bonusChest = false; + @Nullable + private Position spawnPositionOverride; + @Nullable + private Float spawnYawOverride; + @Nullable + private Float spawnPitchOverride; + + private Path parentDirectory = Bukkit.getWorldContainer().toPath(); + /** * Creates an empty WorldCreationOptions for the given world name * @@ -206,6 +217,31 @@ public WorldCreator environment(@NotNull World.Environment env) { return this; } + /** + * Sets the directory that this world's data will be stored in. + * + *

The provided file represents the parent folder used for + * storing all world data (region files, player data, level data, etc.).

+ * + * @param override the parent directory to store this world's data in + * @return this object, for chaining + */ + @NotNull + public WorldCreator parentDirectory(@NotNull Path override) { + this.parentDirectory = override; + return this; + } + + /** + * Gets the directory used for storing this world's data. + * + * @return the parent directory used for world storage + */ + @NotNull + public Path parentDirectory() { + return this.parentDirectory; + } + /** * Gets the type of the world that will be created or loaded * @@ -229,6 +265,83 @@ public WorldCreator type(@NotNull WorldType type) { return this; } + /** + * Sets the forced spawn position for the world created by this {@link WorldCreator}. + *

+ * This overrides vanilla and custom generator behavior without loading any chunks. + * When a forced spawn is specified, the bonus chest will not be generated. + * + * @param position the spawn position + * @param yaw the yaw rotation at spawn + * @param pitch the pitch rotation at spawn + * @return this creator for chaining + */ + @NotNull + public WorldCreator forcedSpawnPosition(@NotNull Position position, float yaw, float pitch) { + this.spawnPositionOverride = position; + this.spawnYawOverride = yaw; + this.spawnPitchOverride = pitch; + return this; + } + + /** + * Clears any previously forced spawn position. + *

+ * After calling this, vanilla spawn selection behavior is used. + * + * @return this creator for chaining + */ + @NotNull + public WorldCreator clearForcedSpawnPosition() { + this.spawnPositionOverride = null; + this.spawnYawOverride = null; + this.spawnPitchOverride = null; + return this; + } + + /** + * Gets the forced spawn position that will be applied when this world is created. + * + *

If this returns {@code null}, vanilla or custom generator behavior will be used + * to determine the spawn position.

+ * + * @return the forced spawn position, or {@code null} to use vanilla behavior + */ + @Nullable + public Position forcedSpawnPosition() { + return this.spawnPositionOverride; + } + + /** + * Gets the forced spawn yaw that will be applied when this world is created. + * + *

If this returns {@code null}, the spawn yaw will be determined by vanilla behavior + * or the world generator.

+ * + *

This value is only meaningful if a forced spawn position is present.

+ * + * @return the forced spawn yaw, or {@code null} to use vanilla behavior + */ + @Nullable + public Float forcedSpawnYaw() { + return this.spawnYawOverride; + } + + /** + * Gets the forced spawn pitch that will be applied when this world is created. + * + *

If this returns {@code null}, the spawn pitch will be determined by vanilla behavior + * or the world generator.

+ * + *

This value is only meaningful if a forced spawn position is present.

+ * + * @return the forced spawn pitch, or {@code null} to use vanilla behavior + */ + @Nullable + public Float forcedSpawnPitch() { + return this.spawnPitchOverride; + } + /** * Gets the generator that will be used to create or load the world. *

diff --git a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch index 9de3cccbff3e..3961874a2e5e 100644 --- a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch +++ b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch @@ -23059,7 +23059,7 @@ index cda915fcb4822689f42b25280eb99aee082ddb74..094d2d528cb74b8f1d277cd780bba7f4 thread1 -> { DedicatedServer dedicatedServer1 = new DedicatedServer( diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index cab1ab613dd081e9472de515a19e1b4738e4fba3..96f7f37c42cecea1d714f7b16276f430fe35d6bc 100644 +index 35f2485b8d727695476531391f886c4a598e486e..c2ffe3c695911c706a257cfbf5c41166545f721d 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -185,7 +185,7 @@ import net.minecraft.world.scores.ScoreboardSaveData; @@ -23165,7 +23165,7 @@ index cab1ab613dd081e9472de515a19e1b4738e4fba3..96f7f37c42cecea1d714f7b16276f430 public MinecraftServer( // CraftBukkit start joptsimple.OptionSet options, -@@ -827,7 +914,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop false : this::haveTime); @@ -23302,7 +23302,7 @@ index cab1ab613dd081e9472de515a19e1b4738e4fba3..96f7f37c42cecea1d714f7b16276f430 this.tickFrame.end(); this.recordEndOfTick(); // Paper - improve tick loop profilerFiller.pop(); -@@ -2559,6 +2678,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent serverLevel.updateLagCompensationTick(); // Paper - lag compensation 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 334018c37150..d2e49c63adae 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -245,7 +245,7 @@ if (profiledDuration != null) { profiledDuration.finish(true); } -@@ -391,31 +_,127 @@ +@@ -391,31 +_,137 @@ } } @@ -366,9 +366,9 @@ + ); + } + this.addLevel(serverLevel); -+ this.initWorld(serverLevel, serverLevelData, worldOptions); ++ this.initWorld(serverLevel, serverLevelData, worldOptions, null); + } -+ public void initWorld(ServerLevel serverLevel, net.minecraft.world.level.storage.PrimaryLevelData serverLevelData, WorldOptions worldOptions) { ++ public void initWorld(ServerLevel serverLevel, net.minecraft.world.level.storage.PrimaryLevelData serverLevelData, WorldOptions worldOptions, org.bukkit.@Nullable WorldCreator worldCreator) { + final boolean isDebugWorld = this.worldData.isDebugWorld(); + if (serverLevel.generator != null) { + serverLevel.getWorld().getPopulators().addAll(serverLevel.generator.getDefaultPopulators(serverLevel.getWorld())); @@ -379,7 +379,17 @@ if (!serverLevelData.isInitialized()) { try { - setInitialSpawn(serverLevel, serverLevelData, worldOptions.generateBonusChest(), isDebugWorld, this.levelLoadListener); -+ setInitialSpawn(serverLevel, serverLevelData, worldOptions.generateBonusChest(), isDebugWorld, serverLevel.levelLoadListener); // Paper - per world level load listener ++ // Paper start - Allow zeroing spawn location ++ if (worldCreator == null || worldCreator.forcedSpawnPosition() == null) { ++ setInitialSpawn(serverLevel, serverLevelData, worldOptions.generateBonusChest(), isDebugWorld, serverLevel.levelLoadListener); // Paper - per world level load listener & rework world loading process ++ } else { ++ serverLevelData.setSpawn(LevelData.RespawnData.of(serverLevel.dimension(), ++ io.papermc.paper.util.MCUtil.toBlockPos(worldCreator.forcedSpawnPosition()), ++ java.util.Objects.requireNonNullElse(worldCreator.forcedSpawnYaw(), 0.0F), ++ java.util.Objects.requireNonNullElse(worldCreator.forcedSpawnPitch(), 0.0F) ++ )); ++ } ++ // Paper end - Allow zeroing spawn location serverLevelData.setInitialized(true); if (isDebugWorld) { this.setupDebugLevel(this.worldData); 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 b2961752b50e..d14b18d27214 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -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; @@ -113,6 +114,7 @@ import net.minecraft.world.level.storage.PlayerDataStorage; import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.level.validation.ContentValidationException; +import net.minecraft.world.level.validation.DirectoryValidator; import org.apache.commons.lang3.StringUtils; import org.bukkit.BanList; import org.bukkit.Bukkit; @@ -1180,7 +1182,7 @@ public World createWorld(WorldCreator creator) { String name = creator.name(); ChunkGenerator chunkGenerator = creator.generator(); BiomeProvider biomeProvider = creator.biomeProvider(); - File folder = new File(this.getWorldContainer(), name); + File folder = new File(creator.parentDirectory().toFile(), name); World world = this.getWorld(name); // Paper start @@ -1214,7 +1216,12 @@ public World createWorld(WorldCreator creator) { LevelStorageSource.LevelStorageAccess levelStorageAccess; try { - levelStorageAccess = LevelStorageSource.createDefault(this.getWorldContainer().toPath()).validateAndCreateAccess(name, actualDimension); + Path serverRoot = this.getWorldContainer().toPath(); + // Make sure parsing off server root for symlinks + DirectoryValidator directoryValidator = LevelStorageSource.parseValidator(serverRoot.resolve("allowed_symlinks.txt")); + LevelStorageSource levelStorageSource = new LevelStorageSource(creator.parentDirectory(), serverRoot.resolve("../backups"), directoryValidator, DataFixers.getDataFixer()); + + levelStorageAccess = levelStorageSource.validateAndCreateAccess(name, actualDimension); } catch (IOException | ContentValidationException ex) { throw new RuntimeException(ex); } @@ -1306,7 +1313,7 @@ public World createWorld(WorldCreator creator) { } this.console.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up - this.console.initWorld(serverLevel, primaryLevelData, primaryLevelData.worldGenOptions()); + this.console.initWorld(serverLevel, primaryLevelData, primaryLevelData.worldGenOptions(), creator); serverLevel.setSpawnSettings(true); // Paper - Put world into worldlist before initing the world; move up