From c47ea834a9eb76d46253be1f1c31d08fdfd1d7ad Mon Sep 17 00:00:00 2001 From: yunuservices Date: Fri, 20 Mar 2026 11:58:14 +0300 Subject: [PATCH 1/2] feat: add folia support to fancyworlds --- plugins/fancyworlds/build.gradle.kts | 5 +- .../commands/world/WorldCreateCMD.java | 24 +- .../commands/world/WorldLoadCMD.java | 32 +- .../commands/world/WorldUnloadCMD.java | 63 +++- .../fancyworlds/main/FancyWorldsPlugin.java | 79 +++- .../worlds/view/FoliaWorldPlatformView.java | 107 ++++++ .../worlds/view/PaperWorldPlatformView.java | 355 ++++++++++++++++++ .../worlds/view/WorldPlatformView.java | 15 + .../src/main/resources/paper-plugin.yml | 3 +- 9 files changed, 637 insertions(+), 46 deletions(-) create mode 100644 plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/FoliaWorldPlatformView.java create mode 100644 plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java create mode 100644 plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/WorldPlatformView.java diff --git a/plugins/fancyworlds/build.gradle.kts b/plugins/fancyworlds/build.gradle.kts index b9f09e332..f8d8c1ef6 100644 --- a/plugins/fancyworlds/build.gradle.kts +++ b/plugins/fancyworlds/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("java-library") id("maven-publish") + id("io.papermc.paperweight.userdev") id("xyz.jpenilla.run-paper") id("com.gradleup.shadow") } @@ -30,7 +31,7 @@ allprojects { } dependencies { - compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + paperweight.foliaDevBundle("1.21.11-R0.1-SNAPSHOT") implementation(project(":plugins:fancyworlds:fw-api")) @@ -119,4 +120,4 @@ val gitCommitMessage: Provider = providers.exec { fun getFWVersion(): String { return file("VERSION").readText() -} \ No newline at end of file +} diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldCreateCMD.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldCreateCMD.java index d2576bb1f..b5dce906a 100644 --- a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldCreateCMD.java +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldCreateCMD.java @@ -2,9 +2,9 @@ import com.fancyinnovations.fancyworlds.api.worlds.WorldService; import com.fancyinnovations.fancyworlds.utils.FancyContext; -import com.fancyinnovations.fancyworlds.utils.WorldFileUtils; import com.fancyinnovations.fancyworlds.worlds.FWorldImpl; import com.fancyinnovations.fancyworlds.worlds.FWorldSettingsImpl; +import com.fancyinnovations.fancyworlds.utils.WorldFileUtils; import org.bukkit.World; import revxrsal.commands.annotation.*; import revxrsal.commands.bukkit.actor.BukkitCommandActor; @@ -59,20 +59,20 @@ public void create( .replace("worldName", name) .send(actor.sender()); - World world = fworld.toWorldCreator().createWorld(); - if (world == null) { + service.registerWorld(fworld); + plugin.getWorldPlatformView().createWorld(fworld).thenAccept(world -> { + fworld.setBukkitWorld(world); + translator.translate("commands.world.create.success") + .withPrefix() + .replace("worldName", name) + .send(actor.sender()); + }).exceptionally(throwable -> { + service.unregisterWorld(fworld); translator.translate("commands.world.create.failed") .withPrefix() .replace("worldName", name) .send(actor.sender()); - return; - } - - fworld.setBukkitWorld(world); - service.registerWorld(fworld); - translator.translate("commands.world.create.success") - .withPrefix() - .replace("worldName", name) - .send(actor.sender()); + return null; + }); } } diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldLoadCMD.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldLoadCMD.java index 9f89134fd..cae2e4e2d 100644 --- a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldLoadCMD.java +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldLoadCMD.java @@ -62,20 +62,30 @@ public void load( .replace("worldName", name) .send(actor.sender()); - World world = fworld.toWorldCreator().createWorld(); - if (world == null) { + final boolean registeredDuringLoad = service.getWorldByName(name) == null; + if (registeredDuringLoad) { + service.registerWorld(fworld); + } + + final FWorldImpl worldToLoad = fworld; + plugin.getWorldPlatformView().createWorld(worldToLoad).thenAccept(world -> { + worldToLoad.setBukkitWorld(world); + if (!registeredDuringLoad) { + service.registerWorld(worldToLoad); + } + translator.translate("commands.world.load.success") + .withPrefix() + .replace("worldName", name) + .send(actor.sender()); + }).exceptionally(throwable -> { + if (registeredDuringLoad) { + service.unregisterWorld(worldToLoad); + } translator.translate("commands.world.load.failed") .withPrefix() .replace("worldName", name) .send(actor.sender()); - return; - } - - fworld.setBukkitWorld(world); - service.registerWorld(fworld); - translator.translate("commands.world.load.success") - .withPrefix() - .replace("worldName", name) - .send(actor.sender()); + return null; + }); } } diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldUnloadCMD.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldUnloadCMD.java index ecbc52c6c..c2d480114 100644 --- a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldUnloadCMD.java +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/commands/world/WorldUnloadCMD.java @@ -16,6 +16,8 @@ import revxrsal.commands.bukkit.actor.BukkitCommandActor; import revxrsal.commands.bukkit.annotation.CommandPermission; +import java.util.concurrent.CompletableFuture; + public class WorldUnloadCMD extends FancyContext { public static final WorldUnloadCMD INSTANCE = new WorldUnloadCMD(); @@ -52,7 +54,7 @@ public void unload( new ConfirmationDialog(question.getMessage()) .withTitle("Confirm unload") - .withOnConfirm(() -> Bukkit.getScheduler().runTask(plugin, () -> unloadImpl(actor, world, true))) + .withOnConfirm(() -> plugin.runGlobalTask(() -> unloadImpl(actor, world, true))) .withOnCancel( () -> translator.translate("commands.world.unload.cancelled") .withPrefix() @@ -70,8 +72,19 @@ private void unloadImpl( final FWorld world, final boolean force ) { + CompletableFuture teleportFuture = CompletableFuture.completedFuture(null); if (world.getBukkitWorld().getPlayerCount() > 0 && force) { - World fallbackWorld = Bukkit.getWorlds().getFirst(); + World fallbackWorld = Bukkit.getWorlds().stream() + .filter(loadedWorld -> !loadedWorld.equals(world.getBukkitWorld())) + .findFirst() + .orElse(null); + if (fallbackWorld == null) { + translator.translate("commands.world.unload.failed") + .withPrefix() + .replace("worldName", world.getName()) + .send(actor.sender()); + return; + } translator.translate("commands.world.unload.teleporting_players") .withPrefix() @@ -80,23 +93,39 @@ private void unloadImpl( .replace("playerCount", String.valueOf(world.getBukkitWorld().getPlayerCount())) .send(actor.sender()); - for (Player player : world.getBukkitWorld().getPlayers()) { - player.teleport(fallbackWorld.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.COMMAND); - } + teleportFuture = CompletableFuture.allOf(world.getBukkitWorld().getPlayers().stream() + .map(player -> player.teleportAsync(fallbackWorld.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.COMMAND) + .thenAccept(success -> { + if (!success) { + player.kick(translator.translate("commands.world.unload.failed") + .replace("worldName", world.getName()) + .buildComponent()); + } + })) + .toArray(CompletableFuture[]::new)); } - if (Bukkit.unloadWorld(world.getBukkitWorld(), true)) { - ((FWorldImpl) world).setBukkitWorld(null); + teleportFuture.thenCompose(ignored -> plugin.getWorldPlatformView().unloadWorld(world.getBukkitWorld(), true)) + .thenAccept(success -> { + if (success) { + ((FWorldImpl) world).setBukkitWorld(null); - translator.translate("commands.world.unload.success") - .withPrefix() - .replace("worldName", world.getName()) - .send(actor.sender()); - } else { - translator.translate("commands.world.unload.failed") - .withPrefix() - .replace("worldName", world.getName()) - .send(actor.sender()); - } + translator.translate("commands.world.unload.success") + .withPrefix() + .replace("worldName", world.getName()) + .send(actor.sender()); + } else { + translator.translate("commands.world.unload.failed") + .withPrefix() + .replace("worldName", world.getName()) + .send(actor.sender()); + } + }).exceptionally(throwable -> { + translator.translate("commands.world.unload.failed") + .withPrefix() + .replace("worldName", world.getName()) + .send(actor.sender()); + return null; + }); } } diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/main/FancyWorldsPlugin.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/main/FancyWorldsPlugin.java index d8119326c..a1167be2b 100644 --- a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/main/FancyWorldsPlugin.java +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/main/FancyWorldsPlugin.java @@ -16,6 +16,9 @@ import com.fancyinnovations.fancyworlds.listeners.WorldUnloadListener; import com.fancyinnovations.fancyworlds.worlds.service.WorldServiceImpl; import com.fancyinnovations.fancyworlds.worlds.storage.fake.FakeWorldStorage; +import com.fancyinnovations.fancyworlds.worlds.view.FoliaWorldPlatformView; +import com.fancyinnovations.fancyworlds.worlds.view.PaperWorldPlatformView; +import com.fancyinnovations.fancyworlds.worlds.view.WorldPlatformView; import de.oliver.fancyanalytics.logger.ExtendedFancyLogger; import de.oliver.fancyanalytics.logger.LogLevel; import de.oliver.fancyanalytics.logger.appender.Appender; @@ -28,6 +31,8 @@ import de.oliver.fancylib.translations.Translator; import de.oliver.fancylib.versionFetcher.FancySpacesVersionFetcher; import de.oliver.fancylib.versionFetcher.VersionFetcher; +import io.papermc.paper.ServerBuildInfo; +import net.kyori.adventure.key.Key; import org.apache.maven.artifact.versioning.ComparableVersion; import org.bukkit.Bukkit; import org.bukkit.GameRule; @@ -38,16 +43,24 @@ import java.io.File; import java.text.SimpleDateFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.function.Supplier; import static java.util.concurrent.CompletableFuture.supplyAsync; public class FancyWorldsPlugin extends JavaPlugin implements FancyWorlds { + private static final long NANOSECONDS_PER_TICK = 50_000_000L; + public static final boolean RUNNING_FOLIA = ServerBuildInfo.buildInfo().isBrandCompatible(Key.key("papermc", "folia")); private static FancyWorldsPlugin INSTANCE; private final ExtendedFancyLogger fancyLogger; + private final WorldPlatformView worldPlatformView = RUNNING_FOLIA + ? new FoliaWorldPlatformView(this) + : new PaperWorldPlatformView(this); private FancyWorldsConfigImpl fancyWorldsConfig; private VersionFetcher versionFetcher; @@ -135,7 +148,7 @@ If you find any bugs, please report them on our discord server (https://discord. """); } - Bukkit.getScheduler().runTaskLaterAsynchronously(INSTANCE, () -> { + runTaskLaterAsynchronously(20L * 20, () -> { if (!Bukkit.getPluginManager().isPluginEnabled("FancyDialogs")) { fancyLogger.error(""" @@ -146,9 +159,8 @@ If you find any bugs, please report them on our discord server (https://discord. -------------------------------------------------- """); Bukkit.getPluginManager().disablePlugin(this); - return; } - }, 20L * 20); // 20s + }); // 20s registerCommands(); @@ -266,6 +278,10 @@ public Translator getTranslator() { return translator; } + public WorldPlatformView getWorldPlatformView() { + return worldPlatformView; + } + @Override public WorldStorage getWorldStorage() { return worldStorage; @@ -275,4 +291,61 @@ public WorldStorage getWorldStorage() { public WorldService getWorldService() { return worldService; } + + public void runGlobalTask(Runnable task) { + supplyGlobal(() -> { + task.run(); + return CompletableFuture.completedFuture(null); + }); + } + + public void runTaskLaterAsynchronously(long delay, Runnable task) { + if (RUNNING_FOLIA) { + getServer().getAsyncScheduler().runDelayed(this, scheduledTask -> task.run(), delay * NANOSECONDS_PER_TICK, TimeUnit.NANOSECONDS); + return; + } + Bukkit.getScheduler().runTaskLaterAsynchronously(this, task, delay); + } + + public CompletableFuture supplyGlobal(Supplier> supplier) { + if (!RUNNING_FOLIA) { + if (Bukkit.isPrimaryThread()) { + try { + return supplier.get(); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + return scheduleBukkitMain(supplier); + } + + if (getServer().isGlobalTickThread()) { + try { + return supplier.get(); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } + + CompletableFuture future = new CompletableFuture<>(); + getServer().getGlobalRegionScheduler().execute(this, () -> completeFuture(future, supplier)); + return future; + } + + private CompletableFuture scheduleBukkitMain(Supplier> supplier) { + CompletableFuture future = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(this, () -> completeFuture(future, supplier)); + return future; + } + + private void completeFuture(CompletableFuture future, Supplier> supplier) { + try { + supplier.get().thenAccept(future::complete).exceptionally(error -> { + future.completeExceptionally(error); + return null; + }); + } catch (Exception e) { + future.completeExceptionally(e); + } + } } diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/FoliaWorldPlatformView.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/FoliaWorldPlatformView.java new file mode 100644 index 000000000..096e6144a --- /dev/null +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/FoliaWorldPlatformView.java @@ -0,0 +1,107 @@ +package com.fancyinnovations.fancyworlds.worlds.view; + +import com.fancyinnovations.fancyworlds.main.FancyWorldsPlugin; +import io.papermc.paper.FeatureHooks; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.event.world.WorldUnloadEvent; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public final class FoliaWorldPlatformView extends PaperWorldPlatformView { + + public FoliaWorldPlatformView(FancyWorldsPlugin plugin) { + super(plugin); + } + + @Override + protected CompletableFuture saveAsync(World world, boolean flush) { + final var futures = new ArrayList>(); + final var level = ((CraftWorld) world).getHandle(); + + level.regioniser.computeForAllRegionsUnsynchronised(region -> { + final var future = new CompletableFuture(); + futures.add(future); + + final var centerChunk = region.getCenterChunk(); + if (centerChunk == null) { + future.complete(null); + return; + } + + plugin().getServer().getRegionScheduler().run(plugin(), world, centerChunk.x, centerChunk.z, task -> { + try { + level.getChunkSource().save(flush); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + }); + + futures.add(saveLevelDataAsync(world)); + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + } + + @Override + public CompletableFuture unloadWorld(World world, boolean save) { + final var handle = ((CraftWorld) world).getHandle(); + final var server = (CraftServer) plugin().getServer(); + + if (server.getServer().getLevel(handle.dimension()) == null) { + return CompletableFuture.completedFuture(false); + } + if (handle.dimension() == net.minecraft.world.level.Level.OVERWORLD) { + return CompletableFuture.completedFuture(false); + } + if (!handle.players().isEmpty()) { + return CompletableFuture.completedFuture(false); + } + + return plugin().supplyGlobal(() -> CompletableFuture.completedFuture(new WorldUnloadEvent(handle.getWorld()).callEvent())) + .thenCompose(success -> { + if (!success) { + return CompletableFuture.completedFuture(false); + } + + final var saving = save ? saveAsync(world, true) : CompletableFuture.completedFuture(null); + return saving.handle((result, throwable) -> plugin().supplyGlobal(() -> { + try { + handle.getChunkSource().close(false); + FeatureHooks.closeEntityManager(handle, save); + handle.levelStorageAccess.close(); + } catch (Exception ignored) { + } + return CompletableFuture.completedFuture(null); + })).thenCompose(self -> self).thenApply(ignored -> { + try { + final var field = server.getClass().getDeclaredField("worlds"); + field.trySetAccessible(); + @SuppressWarnings("unchecked") + final var worlds = (Map) field.get(server); + worlds.remove(world.getName().toLowerCase(Locale.ROOT)); + } catch (NoSuchFieldException | IllegalAccessException e) { + return false; + } + + server.getServer().removeLevel(handle); + handle.regioniser.computeForAllRegionsUnsynchronised(regionThread -> { + if (regionThread.getData().world != handle) { + return; + } + regionThread.getData().getRegionSchedulingHandle().markNonSchedulable(); + }); + + return true; + }); + }); + } + + private FancyWorldsPlugin plugin() { + return FancyWorldsPlugin.get(); + } +} diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java new file mode 100644 index 000000000..0da74c794 --- /dev/null +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java @@ -0,0 +1,355 @@ +package com.fancyinnovations.fancyworlds.worlds.view; + +import com.fancyinnovations.fancyworlds.main.FancyWorldsPlugin; +import com.fancyinnovations.fancyworlds.worlds.FWorldImpl; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.papermc.paper.FeatureHooks; +import io.papermc.paper.world.PaperWorldLoader; +import net.kyori.adventure.key.Key; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.Main; +import net.minecraft.server.WorldLoader; +import net.minecraft.server.dedicated.DedicatedServerProperties; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.ai.village.VillageSiege; +import net.minecraft.world.entity.npc.CatSpawner; +import net.minecraft.world.entity.npc.wanderingtrader.WanderingTraderSpawner; +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.dimension.LevelStem; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.levelgen.PatrolSpawner; +import net.minecraft.world.level.levelgen.PhantomSpawner; +import net.minecraft.world.level.levelgen.WorldDimensions; +import net.minecraft.world.level.levelgen.WorldOptions; +import net.minecraft.world.level.storage.LevelDataAndDimensions; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.validation.ContentValidationException; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.generator.WorldInfo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class PaperWorldPlatformView implements WorldPlatformView { + + private static final Key OVERWORLD = Key.key("overworld"); + private final FancyWorldsPlugin plugin; + + public PaperWorldPlatformView(FancyWorldsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public CompletableFuture createWorld(FWorldImpl world) { + return plugin.supplyGlobal(() -> createWorldInternal(world)); + } + + @Override + public CompletableFuture unloadWorld(World world, boolean save) { + return saveLevelDataAsync(world).thenCompose(ignored -> { + plugin.getServer().allowPausing(plugin, false); + return plugin.supplyGlobal(() -> { + final var dragonBattle = world.getEnderDragonBattle(); + if (!plugin.getServer().unloadWorld(world, save)) { + plugin.getServer().allowPausing(plugin, true); + return CompletableFuture.completedFuture(false); + } + if (dragonBattle != null) { + dragonBattle.getBossBar().removeAll(); + } + plugin.getServer().allowPausing(plugin, true); + return CompletableFuture.completedFuture(true); + }); + }).exceptionally(throwable -> { + plugin.getFancyLogger().warn("Failed to save level data before unloading world " + world.getName()); + return false; + }); + } + + @Override + public boolean isPrimaryWorld(World world) { + return world.key().equals(OVERWORLD); + } + + protected CompletableFuture saveAsync(World world, boolean flush) { + return plugin.supplyGlobal(() -> { + try { + final var level = ((CraftWorld) world).getHandle(); + final var oldSave = level.noSave; + level.noSave = false; + level.save(null, flush, false); + level.noSave = oldSave; + return CompletableFuture.completedFuture(null); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }).thenCompose(ignored -> saveLevelDataAsync(world)); + } + + protected CompletableFuture saveLevelDataAsync(World world) { + final var level = ((CraftWorld) world).getHandle(); + if (level.getDragonFight() != null) { + level.serverLevelData.setEndDragonFightData(level.getDragonFight().saveData()); + } + + level.serverLevelData.setCustomBossEvents(level.getServer().getCustomBossEvents().save(level.registryAccess())); + level.levelStorageAccess.saveDataTag(level.getServer().registryAccess(), level.serverLevelData, level.getServer().getPlayerList().getSingleplayerData()); + + return level.getChunkSource().getDataStorage().scheduleSave().thenApply(ignored -> null); + } + + private CompletableFuture createWorldInternal(FWorldImpl fworld) { + final var server = (CraftServer) plugin.getServer(); + final var console = server.getServer(); + final var directory = plugin.getServer().getWorldContainer().toPath().resolve(fworld.getName()); + final var environment = resolveEnvironment(fworld, directory); + + final ResourceKey levelStemKey; + try { + levelStemKey = resolveLevelStem(environment); + } catch (IllegalArgumentException e) { + return CompletableFuture.failedFuture(e); + } + + try { + Preconditions.checkState(console.getAllLevels().iterator().hasNext(), "Cannot create worlds before the main level is created"); + Preconditions.checkArgument(!Files.exists(directory) || Files.isDirectory(directory), "File (%s) exists and isn't a folder", directory); + Preconditions.checkArgument(server.getWorld(fworld.getName()) == null, "World with name %s already exists", fworld.getName()); + Preconditions.checkState(plugin.getServer().getWorlds().stream() + .map(World::getWorldFolder) + .map(File::toPath) + .noneMatch(directory::equals), + "World with directory %s already exists", directory); + } catch (RuntimeException e) { + return CompletableFuture.failedFuture(e); + } + + final LevelStorageSource.LevelStorageAccess levelStorageAccess; + try { + levelStorageAccess = LevelStorageSource.createDefault(directory.getParent()) + .validateAndCreateAccess(directory.getFileName().toString(), levelStemKey); + } catch (IOException | ContentValidationException e) { + return CompletableFuture.failedFuture(e); + } + + final WorldLoader.DataLoadContext context = console.worldLoaderContext; + RegistryAccess.Frozen registryAccess = context.datapackDimensions(); + Registry levelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM); + + final var levelData = PaperWorldLoader.getLevelData(levelStorageAccess); + if (levelData.fatalError()) { + return CompletableFuture.failedFuture(new IOException("Failed to read level data for world " + fworld.getName())); + } + + final var dataTag = levelData.dataTag(); + final PrimaryLevelData primaryLevelData; + if (dataTag != null) { + final LevelDataAndDimensions levelDataAndDimensions = LevelStorageSource.getLevelDataAndDimensions( + dataTag, + context.dataConfiguration(), + levelStemRegistry, + context.datapackWorldgen() + ); + primaryLevelData = (PrimaryLevelData) levelDataAndDimensions.worldData(); + registryAccess = levelDataAndDimensions.dimensions().dimensionsRegistryAccess(); + } else { + final LevelSettings levelSettings = new LevelSettings( + fworld.getName(), + GameType.byId(server.getDefaultGameMode().getValue()), + plugin.getServer().isHardcore(), + Difficulty.EASY, + false, + new GameRules(context.dataConfiguration().enabledFeatures()), + context.dataConfiguration() + ); + + final WorldOptions worldOptions = new WorldOptions(fworld.getSeed(), fworld.canGenerateStructures(), false); + final var properties = new DedicatedServerProperties.WorldDimensionData( + createGeneratorSettings(fworld.getGenerator()), + resolveGeneratorPresetName(fworld.getGenerator()) + ); + final WorldDimensions.Complete complete = properties.create(context.datapackWorldgen()).bake(levelStemRegistry); + + primaryLevelData = new PrimaryLevelData( + levelSettings, + worldOptions, + complete.specialWorldProperty(), + complete.lifecycle().add(context.datapackWorldgen().allRegistriesLifecycle()) + ); + registryAccess = complete.dimensionsRegistryAccess(); + } + + levelStemRegistry = registryAccess.lookupOrThrow(Registries.LEVEL_STEM); + primaryLevelData.customDimensions = levelStemRegistry; + primaryLevelData.checkName(fworld.getName()); + primaryLevelData.setModdedInfo(console.getServerModName(), console.getModdedStatus().shouldReportAsModified()); + + if (console.options.has("forceUpgrade")) { + Main.forceUpgrade( + levelStorageAccess, + primaryLevelData, + DataFixers.getDataFixer(), + console.options.has("eraseCache"), + () -> true, + registryAccess, + console.options.has("recreateRegionFiles") + ); + } + + final long seed = BiomeManager.obfuscateSeed(primaryLevelData.worldGenOptions().seed()); + final List spawners = environment == World.Environment.NORMAL + ? ImmutableList.of( + new PhantomSpawner(), + new PatrolSpawner(), + new CatSpawner(), + new VillageSiege(), + new WanderingTraderSpawner(primaryLevelData) + ) + : ImmutableList.of(); + + final LevelStem stem = levelStemRegistry.getValueOrThrow(levelStemKey); + final WorldInfo worldInfo = new org.bukkit.craftbukkit.generator.CraftWorldInfo( + primaryLevelData, + levelStorageAccess, + environment, + stem.type().value(), + stem.generator(), + console.registryAccess() + ); + + final ResourceKey dimensionKey = resolveDimensionKey(server, fworld.getName(), environment); + final ServerLevel serverLevel = new ServerLevel( + console, + console.executor, + levelStorageAccess, + primaryLevelData, + dimensionKey, + stem, + primaryLevelData.isDebugWorld(), + seed, + spawners, + true, + console.overworld().getRandomSequences(), + environment, + null, + null + ); + + if (server.getWorld(fworld.getName()) == null) { + return CompletableFuture.failedFuture(new IllegalStateException("World " + fworld.getName() + " was not properly memoized")); + } + + console.addLevel(serverLevel); + console.initWorld(serverLevel, primaryLevelData, primaryLevelData.worldGenOptions()); + serverLevel.setSpawnSettings(true); + FeatureHooks.tickEntityManager(serverLevel); + console.prepareLevel(serverLevel); + + return CompletableFuture.completedFuture(serverLevel.getWorld()); + } + + private World.Environment resolveEnvironment(FWorldImpl world, Path directory) { + if (Files.isDirectory(directory.resolve("DIM1")) && !Files.isDirectory(directory.resolve("DIM-1"))) { + return World.Environment.THE_END; + } + if (Files.isDirectory(directory.resolve("DIM-1")) && !Files.isDirectory(directory.resolve("DIM1"))) { + return World.Environment.NETHER; + } + return world.getEnvironment(); + } + + private ResourceKey resolveLevelStem(World.Environment environment) { + return switch (environment) { + case NORMAL -> LevelStem.OVERWORLD; + case NETHER -> LevelStem.NETHER; + case THE_END -> LevelStem.END; + default -> throw new IllegalArgumentException("Environment " + environment + " is not supported"); + }; + } + + private ResourceKey resolveDimensionKey(CraftServer server, String worldName, World.Environment environment) { + final String levelName = server.getServer().getProperties().levelName; + + if (environment == World.Environment.NETHER && worldName.equals(levelName + "_nether")) { + return net.minecraft.world.level.Level.NETHER; + } + if (environment == World.Environment.THE_END && worldName.equals(levelName + "_the_end")) { + return net.minecraft.world.level.Level.END; + } + + return ResourceKey.create( + Registries.DIMENSION, + Identifier.fromNamespaceAndPath("fancyworlds", createWorldKey(worldName)) + ); + } + + private String createWorldKey(String worldName) { + return worldName.toLowerCase(Locale.ROOT) + .replaceAll("[^a-z0-9_\\-./ ]+", "") + .replace(" ", "_"); + } + + private JsonObject createGeneratorSettings(String generator) { + final JsonObject root = new JsonObject(); + root.addProperty("biome", "minecraft:plains"); + root.addProperty("lakes", false); + root.addProperty("features", false); + root.addProperty("decoration", false); + + final JsonArray layers = new JsonArray(); + layers.add(createLayer("minecraft:bedrock", 1)); + layers.add(createLayer("minecraft:dirt", 2)); + layers.add(createLayer("minecraft:grass_block", 1)); + root.add("layers", layers); + + if ("flat".equalsIgnoreCase(generator)) { + final JsonArray structures = new JsonArray(); + structures.add("minecraft:villages"); + root.add("structure_overrides", structures); + } + + return root; + } + + private JsonObject createLayer(String block, int height) { + final JsonObject layer = new JsonObject(); + layer.addProperty("block", block); + layer.addProperty("height", height); + return layer; + } + + private String resolveGeneratorPresetName(String generator) { + if (generator == null) { + return "normal"; + } + + return switch (generator.toLowerCase(Locale.ROOT)) { + case "flat" -> "flat"; + case "amplified" -> "amplified"; + case "large_biomes" -> "large_biomes"; + default -> "normal"; + }; + } +} diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/WorldPlatformView.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/WorldPlatformView.java new file mode 100644 index 000000000..91f99e117 --- /dev/null +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/WorldPlatformView.java @@ -0,0 +1,15 @@ +package com.fancyinnovations.fancyworlds.worlds.view; + +import com.fancyinnovations.fancyworlds.worlds.FWorldImpl; +import org.bukkit.World; + +import java.util.concurrent.CompletableFuture; + +public interface WorldPlatformView { + + CompletableFuture createWorld(FWorldImpl world); + + CompletableFuture unloadWorld(World world, boolean save); + + boolean isPrimaryWorld(World world); +} diff --git a/plugins/fancyworlds/src/main/resources/paper-plugin.yml b/plugins/fancyworlds/src/main/resources/paper-plugin.yml index a3d181b4c..d961352d0 100644 --- a/plugins/fancyworlds/src/main/resources/paper-plugin.yml +++ b/plugins/fancyworlds/src/main/resources/paper-plugin.yml @@ -8,10 +8,11 @@ website: "https://fancyinnovations.com/docs/minecraft-plugins/fancyworlds" authors: - "OliverSchlueter" api-version: "1.21" +folia-supported: true load: STARTUP dependencies: server: FancyDialogs: load: BEFORE required: true - join-classpath: true \ No newline at end of file + join-classpath: true From 5210ef79bec15c4624042b2aa2d72c501f07613a Mon Sep 17 00:00:00 2001 From: yunuservices Date: Fri, 20 Mar 2026 12:23:28 +0300 Subject: [PATCH 2/2] fix: preserve legacy world creation behavior --- .../worlds/view/PaperWorldPlatformView.java | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java index 0da74c794..282e8a02b 100644 --- a/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java +++ b/plugins/fancyworlds/src/main/java/com/fancyinnovations/fancyworlds/worlds/view/PaperWorldPlatformView.java @@ -39,17 +39,22 @@ import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.level.validation.ContentValidationException; import org.bukkit.World; +import org.bukkit.WorldCreator; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.WorldInfo; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; public class PaperWorldPlatformView implements WorldPlatformView { @@ -63,6 +68,9 @@ public PaperWorldPlatformView(FancyWorldsPlugin plugin) { @Override public CompletableFuture createWorld(FWorldImpl world) { + if (shouldUseLegacyCreator(world)) { + return createLegacyWorld(world); + } return plugin.supplyGlobal(() -> createWorldInternal(world)); } @@ -125,6 +133,7 @@ private CompletableFuture createWorldInternal(FWorldImpl fworld) { final var console = server.getServer(); final var directory = plugin.getServer().getWorldContainer().toPath().resolve(fworld.getName()); final var environment = resolveEnvironment(fworld, directory); + final ChunkGenerator chunkGenerator = resolveChunkGenerator(fworld); final ResourceKey levelStemKey; try { @@ -238,6 +247,7 @@ private CompletableFuture createWorldInternal(FWorldImpl fworld) { stem.generator(), console.registryAccess() ); + final BiomeProvider biomeProvider = chunkGenerator != null ? chunkGenerator.getDefaultBiomeProvider(worldInfo) : null; final ResourceKey dimensionKey = resolveDimensionKey(server, fworld.getName(), environment); final ServerLevel serverLevel = new ServerLevel( @@ -253,8 +263,8 @@ private CompletableFuture createWorldInternal(FWorldImpl fworld) { true, console.overworld().getRandomSequences(), environment, - null, - null + chunkGenerator, + biomeProvider ); if (server.getWorld(fworld.getName()) == null) { @@ -270,6 +280,41 @@ private CompletableFuture createWorldInternal(FWorldImpl fworld) { return CompletableFuture.completedFuture(serverLevel.getWorld()); } + private boolean shouldUseLegacyCreator(FWorldImpl world) { + return world.getEnvironment() == World.Environment.CUSTOM; + } + + private CompletableFuture createLegacyWorld(FWorldImpl world) { + return plugin.supplyGlobal(() -> { + try { + final World created = world.toWorldCreator().createWorld(); + if (created == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Failed to create world " + world.getName() + " via WorldCreator")); + } + return CompletableFuture.completedFuture(created); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } + + private ChunkGenerator resolveChunkGenerator(FWorldImpl world) { + if (world.getGenerator() == null) { + return null; + } + if (isBuiltInGenerator(world.getGenerator())) { + return null; + } + return WorldCreator.getGeneratorForName(world.getName(), world.getGenerator(), plugin.getServer().getConsoleSender()); + } + + private boolean isBuiltInGenerator(String generator) { + return switch (generator.toLowerCase(Locale.ROOT)) { + case "", "default", "normal", "flat", "amplified", "large_biomes" -> true; + default -> false; + }; + } + private World.Environment resolveEnvironment(FWorldImpl world, Path directory) { if (Files.isDirectory(directory.resolve("DIM1")) && !Files.isDirectory(directory.resolve("DIM-1"))) { return World.Environment.THE_END; @@ -306,9 +351,21 @@ private ResourceKey resolveDimensionKey(CraftSe } private String createWorldKey(String worldName) { - return worldName.toLowerCase(Locale.ROOT) - .replaceAll("[^a-z0-9_\\-./ ]+", "") - .replace(" ", "_"); + String sanitized = worldName.toLowerCase(Locale.ROOT) + .replace(" ", "_") + .replaceAll("[^a-z0-9_\\-./]+", ""); + if (sanitized.isBlank()) { + sanitized = "world"; + } + if (sanitized.length() > 48) { + sanitized = sanitized.substring(0, 48); + } + + final String uniqueSuffix = UUID.nameUUIDFromBytes(worldName.getBytes(StandardCharsets.UTF_8)) + .toString() + .replace("-", "") + .substring(0, 12); + return sanitized + "_" + uniqueSuffix; } private JsonObject createGeneratorSettings(String generator) {