world: use World#getWorldFolder as the dimension folder; queue seeding
Build / build (push) Successful in 1m5s

On Paper 26.x getWorldFolder() already points at <root>/dimensions/<ns>/<path>,
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
This commit is contained in:
2026-09-07 00:11:45 +03:00
parent 74dd2438e1
commit 93164378e2
3 changed files with 64 additions and 35 deletions
@@ -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<String> onDone) {}
private final java.util.Queue<SeedJob> 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<String> 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();
}
}
@@ -124,10 +124,6 @@ public final class MapSyncCommand implements TabExecutor {
sender.sendMessage("Usage: /mapsync seed <world|all>");
return;
}
if (plugin.isSeeding()) {
sender.sendMessage("A seed run is already in progress.");
return;
}
List<TrackedWorld> 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<TrackedWorld> 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) {
@@ -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
// (<root>/dimensions/<ns>/<path>), 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<World> 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<Path> 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/<ns>/<path>} 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