From 93164378e2250801a00fe2c8162baa21daef42f0 Mon Sep 17 00:00:00 2001 From: Janis Jansons Date: Mon, 7 Sep 2026 00:11:45 +0300 Subject: [PATCH] world: use World#getWorldFolder as the dimension folder; queue seeding On Paper 26.x getWorldFolder() already points at /dimensions//, so anchoring DimensionType.getStorageFolder on it doubled the tail. Use the folder directly when region/ exists, otherwise anchor on the stripped level root. Seeding now queues worlds instead of silently skipping every world after the first when several need their initial InhabitedTime scan at the same start. Claude-Session: https://claude.ai/code/session_011FePLXwBsCGLTzaSkk1Z6V --- .../janhouse/mapsyncer/MapSyncerPlugin.java | 40 +++++++++++++++---- .../mapsyncer/cmd/MapSyncCommand.java | 29 ++++---------- .../mapsyncer/world/TrackedWorld.java | 30 ++++++++++---- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/main/java/lv/janhouse/mapsyncer/MapSyncerPlugin.java b/src/main/java/lv/janhouse/mapsyncer/MapSyncerPlugin.java index d9daaca..471d268 100644 --- a/src/main/java/lv/janhouse/mapsyncer/MapSyncerPlugin.java +++ b/src/main/java/lv/janhouse/mapsyncer/MapSyncerPlugin.java @@ -269,15 +269,39 @@ public final class MapSyncerPlugin extends JavaPlugin implements Listener { return cycleRunning.get(); } - /** Runs the InhabitedTime seeder for one world in the background. */ + private record SeedJob(TrackedWorld world, java.util.function.Consumer onDone) {} + + private final java.util.Queue seedQueue = new java.util.concurrent.ConcurrentLinkedQueue<>(); + + /** + * Queues the InhabitedTime seeder for one world. Worlds are scanned one at + * a time in the background (each scan reads every region file once). + */ public boolean seedAsync(TrackedWorld world, java.util.function.Consumer onDone) { + for (SeedJob queued : seedQueue) { + if (queued.world().name().equals(world.name())) { + return false; + } + } + seedQueue.add(new SeedJob(world, onDone)); + drainSeedQueue(); + return true; + } + + private void drainSeedQueue() { + SeedJob job = seedQueue.peek(); + if (job == null) { + return; + } DiscoverySeeder seeder = new DiscoverySeeder(discovery); if (!activeSeeder.compareAndSet(null, seeder)) { - return false; + return; // the running job drains the queue when it finishes } + seedQueue.poll(); + TrackedWorld world = job.world(); long minTicks = settings().seedMinInhabitedTicks; getLogger().info("Seeding discovery for '" + world.name() + "' from InhabitedTime >= " + minTicks - + " (one-off scan of " + world.regionDir() + ")..."); + + " (one-off scan of " + world.regionDir() + ", " + seedQueue.size() + " more queued)..."); Bukkit.getScheduler().runTaskAsynchronously(this, () -> { try { DiscoverySeeder.Result r = seeder.seed(world, minTicks, msg -> getLogger().info(msg)); @@ -287,17 +311,19 @@ public final class MapSyncerPlugin extends JavaPlugin implements Listener { String summary = "Seeded '" + world.name() + "': " + r.chunksSeeded() + " of " + r.chunksSeen() + " chunks in " + r.files() + " region files" + (r.cancelled() ? " (cancelled)" : ""); getLogger().info(summary); - if (onDone != null) { - onDone.accept(summary); + if (job.onDone() != null) { + job.onDone().accept(summary); } } finally { activeSeeder.compareAndSet(seeder, null); + if (isEnabled()) { + drainSeedQueue(); + } } }); - return true; } public boolean isSeeding() { - return activeSeeder.get() != null; + return activeSeeder.get() != null || !seedQueue.isEmpty(); } } diff --git a/src/main/java/lv/janhouse/mapsyncer/cmd/MapSyncCommand.java b/src/main/java/lv/janhouse/mapsyncer/cmd/MapSyncCommand.java index 6d30e13..02c1ce2 100644 --- a/src/main/java/lv/janhouse/mapsyncer/cmd/MapSyncCommand.java +++ b/src/main/java/lv/janhouse/mapsyncer/cmd/MapSyncCommand.java @@ -124,10 +124,6 @@ public final class MapSyncCommand implements TabExecutor { sender.sendMessage("Usage: /mapsync seed "); return; } - if (plugin.isSeeding()) { - sender.sendMessage("A seed run is already in progress."); - return; - } List targets = new ArrayList<>(); if (args[1].equalsIgnoreCase("all")) { targets.addAll(plugin.trackedWorlds()); @@ -139,24 +135,15 @@ public final class MapSyncCommand implements TabExecutor { } targets.add(w); } - seedNext(sender, targets, 0); - } - - private void seedNext(CommandSender sender, List targets, int index) { - if (index >= targets.size()) { - sender.sendMessage("Seeding finished."); - return; - } - TrackedWorld w = targets.get(index); - boolean started = plugin.seedAsync(w, summary -> Bukkit.getScheduler().runTask(plugin, () -> { - sender.sendMessage(summary); - seedNext(sender, targets, index + 1); - })); - if (started) { - sender.sendMessage("Seeding '" + w.name() + "' in the background..."); - } else { - sender.sendMessage("A seed run is already in progress."); + int queued = 0; + for (TrackedWorld w : targets) { + boolean accepted = plugin.seedAsync(w, summary -> + Bukkit.getScheduler().runTask(plugin, () -> sender.sendMessage(summary))); + if (accepted) { + queued++; + } } + sender.sendMessage("Queued " + queued + " world(s) for seeding; they run one at a time in the background."); } private void reload(CommandSender sender) { diff --git a/src/main/java/lv/janhouse/mapsyncer/world/TrackedWorld.java b/src/main/java/lv/janhouse/mapsyncer/world/TrackedWorld.java index 8cca194..5d09b1f 100644 --- a/src/main/java/lv/janhouse/mapsyncer/world/TrackedWorld.java +++ b/src/main/java/lv/janhouse/mapsyncer/world/TrackedWorld.java @@ -73,12 +73,21 @@ public final class TrackedWorld { * one of them does, so a migrated server keeps working. */ static Path findRegionDir(World world) { + // On Paper 26.x World#getWorldFolder already IS the dimension folder + // (/dimensions//), so region/ sits directly under it. + Path own = world.getWorldFolder().toPath(); + if (Files.isDirectory(own.resolve("region"))) { + return own.resolve("region"); + } + // Not saved yet, or an older layout: ask the server, anchored at the + // level root (the main world's folder with any dimensions/… tail removed). List worlds = Bukkit.getWorlds(); - Path main = worlds.isEmpty() ? world.getWorldFolder().toPath() : worlds.get(0).getWorldFolder().toPath(); + Path mainFolder = worlds.isEmpty() ? own : worlds.get(0).getWorldFolder().toPath(); + Path root = levelRoot(mainFolder); Path nms = null; try { var level = ((org.bukkit.craftbukkit.CraftWorld) world).getHandle(); - nms = net.minecraft.world.level.dimension.DimensionType.getStorageFolder(level.dimension(), main) + nms = net.minecraft.world.level.dimension.DimensionType.getStorageFolder(level.dimension(), root) .resolve("region"); } catch (RuntimeException | LinkageError e) { // Fall through to probing. @@ -89,21 +98,28 @@ public final class TrackedWorld { List candidates = new ArrayList<>(); String ns = world.getKey().getNamespace(); String path = world.getKey().getKey(); - Path own = world.getWorldFolder().toPath(); - candidates.add(main.resolve("dimensions").resolve(ns).resolve(path).resolve("region")); - candidates.add(own.resolve("dimensions").resolve(ns).resolve(path).resolve("region")); + candidates.add(root.resolve("dimensions").resolve(ns).resolve(path).resolve("region")); switch (world.getEnvironment()) { case NETHER -> candidates.add(own.resolve("DIM-1").resolve("region")); case THE_END -> candidates.add(own.resolve("DIM1").resolve("region")); default -> { } } - candidates.add(own.resolve("region")); for (Path c : candidates) { if (Files.isDirectory(c)) { return c; } } - return nms != null ? nms : candidates.get(0); + return nms != null ? nms : own.resolve("region"); + } + + /** Strips a trailing {@code dimensions//} from a world folder, if present. */ + static Path levelRoot(Path folder) { + Path abs = folder.toAbsolutePath().normalize(); + int n = abs.getNameCount(); + if (n >= 4 && abs.getName(n - 3).toString().equals("dimensions")) { + return abs.getParent().getParent().getParent(); + } + return abs; } // ---------------------------------------------------------------- layers