world: resolve the region directory through the server, not by probing
Build / build (push) Successful in 1m20s

Paper 26.x stores every dimension under <main>/dimensions/<ns>/<path>/,
and the directory does not exist until the first save, so a brand-new world
was reported as "no region directory found" and never tracked. Ask
DimensionType.getStorageFolder for the exact path and accept that it may not
exist yet; the legacy layouts are still probed for migrated servers.

The seeder now treats a missing directory as an empty world.

Claude-Session: https://claude.ai/code/session_011FePLXwBsCGLTzaSkk1Z6V
This commit is contained in:
2026-09-07 00:07:44 +03:00
parent 7fb3a82482
commit 74dd2438e1
2 changed files with 27 additions and 9 deletions
@@ -45,6 +45,10 @@ public final class DiscoverySeeder {
public Result seed(TrackedWorld world, long minInhabitedTicks, Consumer<String> log) {
List<Path> files = new ArrayList<>();
if (!Files.isDirectory(world.regionDir())) {
// Brand-new world: nothing has been saved yet.
return new Result(0, 0, 0, false);
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(world.regionDir(), "r.*.mca")) {
for (Path p : stream) {
files.add(p);
@@ -63,20 +63,34 @@ public final class TrackedWorld {
// ---------------------------------------------------------------- layout
/**
* Locates the {@code region/} directory. Paper 26.x keeps every dimension,
* the main overworld included, under {@code <main>/dimensions/<ns>/<path>/};
* older layouts are probed after that so a migrated server still works.
* Locates the {@code region/} directory.
*
* <p>Asks the server for the dimension's storage folder (Paper 26.x keeps
* every dimension, the main overworld and Bukkit's extra worlds included,
* under {@code <main>/dimensions/<ns>/<path>/}). The directory may not exist
* yet for a brand-new world; that is fine, it simply has no region files.
* Older layouts are probed only if the server's answer does not exist and
* one of them does, so a migrated server keeps working.
*/
static Path findRegionDir(World world) {
List<World> worlds = Bukkit.getWorlds();
Path main = worlds.isEmpty() ? world.getWorldFolder().toPath() : worlds.get(0).getWorldFolder().toPath();
Path nms = null;
try {
var level = ((org.bukkit.craftbukkit.CraftWorld) world).getHandle();
nms = net.minecraft.world.level.dimension.DimensionType.getStorageFolder(level.dimension(), main)
.resolve("region");
} catch (RuntimeException | LinkageError e) {
// Fall through to probing.
}
if (nms != null && Files.isDirectory(nms)) {
return nms;
}
List<Path> candidates = new ArrayList<>();
String ns = world.getKey().getNamespace();
String path = world.getKey().getKey();
List<World> worlds = Bukkit.getWorlds();
Path main = worlds.isEmpty() ? null : worlds.get(0).getWorldFolder().toPath();
Path own = world.getWorldFolder().toPath();
if (main != null) {
candidates.add(main.resolve("dimensions").resolve(ns).resolve(path).resolve("region"));
}
candidates.add(main.resolve("dimensions").resolve(ns).resolve(path).resolve("region"));
candidates.add(own.resolve("dimensions").resolve(ns).resolve(path).resolve("region"));
switch (world.getEnvironment()) {
case NETHER -> candidates.add(own.resolve("DIM-1").resolve("region"));
@@ -89,7 +103,7 @@ public final class TrackedWorld {
return c;
}
}
return null;
return nms != null ? nms : candidates.get(0);
}
// ---------------------------------------------------------------- layers