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