Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/src/admin/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,57 @@

Check startup: the console should confirm uSkyBlock loaded without dependency errors, and `/island` should work in-game.

## World loading and the void generator

uSkyBlock creates the skyblock world (`options.general.worldName`, default `skyworld`)
automatically and attaches its void chunk generator every time the server starts. The
generator association is **not** stored in the world data — whenever the world is loaded
without it, new chunks generate regular vanilla terrain instead of void.

Most setups need no extra configuration. Two setups do:

### The bukkit.yml generator mapping

uSkyBlock registers its generator in the server's `bukkit.yml` automatically when it sets
up its worlds:

```yaml
worlds:
skyworld:
generator: uSkyBlock
skyworld_nether:
generator: uSkyBlock
```

This entry is the only mechanism the server consults on *every* load path — including when
the skyblock world is the default world (`level-name` in `server.properties`), which loads
before any plugin can attach a generator. uSkyBlock only adds missing entries; it never
overwrites a generator you configured yourself. If your `bukkit.yml` is not writable
(read-only container images, managed hosting), add the entry above manually — adjust the
world names if you changed `worldName`.

Note for default-world setups: the entry takes effect at the *next* start, so the very
first start of a brand-new setup with `level-name` pointing at the skyblock world will
still log the generator warning once — restart and the warning goes away; chunks generated
during that first start keep their vanilla terrain (see recovery below).

### Multiverse

uSkyBlock imports its worlds into Multiverse-Core automatically, including the correct
generator. If a world is registered in Multiverse without a generator (for example from a
manual `/mv import` without `-g uSkyBlock`), uSkyBlock repairs the registration at startup;
the repair takes effect the next time the world is loaded.

### Symptoms and recovery

If the world was ever loaded without the generator, vanilla terrain appears around islands
while the islands themselves look normal, and uSkyBlock logs a severe warning at startup.
Fix the cause as described above, then remove already generated terrain with
`/usb chunk regen <x> <z> <radius>` while standing in the skyblock world — or delete the
affected region files while the server is stopped. Careful: `x`, `z`, and `radius` are
**chunk** coordinates (not block coordinates), and regeneration resets every block in the
affected chunks — including island builds — so keep the radius clear of islands.

## First setting to check

Config lives in `plugins/uSkyBlock/config.yml`.
Expand Down
5 changes: 5 additions & 0 deletions uSkyBlock-Core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ dependencies {
compileOnly(libs.net.milkbowl.vault.vaultapi)
compileOnly(libs.org.spigotmc.spigot.api)
compileOnly(libs.org.mvplugins.multiverse.core.multiverse.core)
testImplementation(libs.org.mvplugins.multiverse.core.multiverse.core) {
// Tests only need Multiverse's own (self-contained) classes; its transitive
// dependencies include JitPack-only artifacts unavailable from our repositories.
isTransitive = false
}
compileOnly(libs.org.mvplugins.multiverse.inventories.multiverse.inventories)
compileOnly(libs.com.sk89q.worldedit.worldedit.bukkit)
testImplementation(libs.com.sk89q.worldedit.worldedit.bukkit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public void registerOverworld(@NotNull World world) {
}

MultiverseWorld mvWorld = mvWorldManager.getWorld(world).get();
ensureGeneratorRegistered(mvWorldManager, mvWorld);
mvWorld.setScale(1.0);

if (runtimeConfig.general().spawnSize() > 0 && LocationUtil.isEmptyLocation(mvWorld.getSpawnLocation())) {
Expand Down Expand Up @@ -98,6 +99,7 @@ public void registerNetherworld(@NotNull World world) {
}

MultiverseWorld mvWorld = mvWorldManager.getWorld(world).get();
ensureGeneratorRegistered(mvWorldManager, mvWorld);
mvWorld.setScale(1.0);

if (runtimeConfig.general().spawnSize() > 0 && LocationUtil.isEmptyLocation(mvWorld.getSpawnLocation())) {
Expand All @@ -112,4 +114,28 @@ public void registerNetherworld(@NotNull World world) {
mvWorld.setRespawnWorld(plugin.getWorldManager().getWorld().getName());
}
}

/**
* Ensures the Multiverse registration for the given world has a generator configured.
* Multiverse re-applies the stored generator string every time it loads the world; without
* it, the world loads with the vanilla generator and new chunks get regular terrain.
* The repair takes effect the next time Multiverse loads the world.
*
* @param mvWorldManager Multiverse world manager used to persist the change.
* @param mvWorld Multiverse world registration to check.
*/
void ensureGeneratorRegistered(@NotNull WorldManager mvWorldManager, @NotNull MultiverseWorld mvWorld) {
String generator = mvWorld.getGenerator();
if (generator == null || generator.isEmpty()) {
logger.warning("Multiverse world '" + mvWorld.getName() + "' has no generator configured. "
+ "Setting it to '" + GENERATOR_NAME + "' so it loads as a void world.");
mvWorld.getStringPropertyHandle().setProperty("generator", GENERATOR_NAME)
.flatMap(ignored -> mvWorldManager.saveWorldsConfig())
.onFailure(failure -> logger.severe("Failed to set Multiverse generator for '"
+ mvWorld.getName() + "': " + failure));
} else if (!generator.equals(GENERATOR_NAME)) {
logger.warning("Multiverse world '" + mvWorld.getName() + "' uses generator '" + generator
+ "' instead of '" + GENERATOR_NAME + "'. Leaving it unchanged - make sure it is a void generator.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package us.talabrek.ultimateskyblock.world;

import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;

/**
* Maintains the {@code worlds.<name>.generator} mapping in the server's bukkit.yml.
* The plugin generator association is not stored in the world data, and the bukkit.yml
* mapping is the only mechanism the server consults on every load path - including the
* default (level-name) world, which loads before any plugin can attach a generator.
* Only missing entries are added; existing values are never overwritten. Changes take
* effect on the next server start.
*/
public class BukkitYmlGeneratorMapping {

private static final String GENERATOR_NAME = "uSkyBlock";

private final Path bukkitYml;
private final Logger logger;

public BukkitYmlGeneratorMapping(@NotNull Path bukkitYml, @NotNull Logger logger) {
this.bukkitYml = bukkitYml;
this.logger = logger;
}

/**
* Ensures bukkit.yml maps the given world to the uSkyBlock generator. Adds the entry
* when missing, leaves any existing generator untouched, and only logs on failure.
*
* @param worldName Name of the world to register the generator for.
*/
public void ensureMapping(@NotNull String worldName) {
if (!Files.exists(bukkitYml)) {
logger.warning("No bukkit.yml found at " + bukkitYml.toAbsolutePath()
+ ", cannot register the world generator for '" + worldName + "'.");
return;
}

YamlConfiguration config = new YamlConfiguration();
try {
config.load(bukkitYml.toFile());
} catch (IOException | InvalidConfigurationException e) {
logger.warning("Could not read bukkit.yml to register the generator for '"
+ worldName + "': " + e.getMessage());
return;
}
String path = "worlds." + worldName + ".generator";
String existing = config.getString(path);
if (existing == null || existing.isEmpty()) {
config.set(path, GENERATOR_NAME);
try {
config.save(bukkitYml.toFile());
logger.info("Registered generator '" + GENERATOR_NAME + "' for world '" + worldName
+ "' in bukkit.yml, so the world keeps its void generator on every load path.");
} catch (IOException e) {
logger.warning("Could not write bukkit.yml to register the generator for '"
+ worldName + "': " + e.getMessage());
}
} else if (!existing.equals(GENERATOR_NAME)) {
logger.warning("bukkit.yml maps world '" + worldName + "' to generator '" + existing
+ "' instead of '" + GENERATOR_NAME + "'. Leaving it unchanged - make sure it is a void generator.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -40,6 +41,7 @@ public class WorldManager {
private final RuntimeConfigs runtimeConfigs;
private final Scheduler scheduler;
private final Logger logger;
private final BukkitYmlGeneratorMapping bukkitYmlGeneratorMapping;

public static volatile World skyBlockWorld;
public static volatile World skyBlockNetherWorld;
Expand All @@ -61,6 +63,8 @@ public WorldManager(
this.runtimeConfigs = runtimeConfigs;
this.logger = logger;
this.scheduler = scheduler;
// bukkit.yml resolves against the process working directory, same as the server's own lookup
this.bukkitYmlGeneratorMapping = new BukkitYmlGeneratorMapping(Path.of("bukkit.yml"), logger);
}

/**
Expand Down Expand Up @@ -211,6 +215,50 @@ public ChunkGenerator getDefaultWorldGenerator(@NotNull String worldName, @Nulla
: getOverworldGenerator();
}

/**
* Checks whether the given {@link World} has the expected {@link ChunkGenerator} attached.
* A world loaded by the server (default world) or another plugin without a generator keeps
* the vanilla generator; Bukkit's createWorld() cannot change it afterwards.
*
* @param world World to check.
* @param expected Generator the world should be using.
* @return True if the attached generator is of the expected class, false otherwise.
*/
static boolean hasExpectedGenerator(@NotNull World world, @NotNull ChunkGenerator expected) {
ChunkGenerator actual = world.getGenerator();
return actual != null && actual.getClass().getName().equals(expected.getClass().getName());
}

/**
* Builds the admin-facing warning logged when a skyblock world runs without the void generator.
*
* @param worldName Name of the affected world.
* @return Warning message, one entry per log line.
*/
static List<String> wrongGeneratorWarning(@NotNull String worldName) {
return List.of(
"============================================================",
"World '" + worldName + "' is loaded WITHOUT the uSkyBlock void",
"generator. New chunks will generate regular vanilla terrain!",
"This happens when the world is loaded before uSkyBlock can",
"attach its generator. uSkyBlock registers the generator in",
"bukkit.yml and Multiverse automatically - usually a RESTART",
"fixes this. If the warning persists after a restart:",
"- Ensure bukkit.yml contains (and is writable):",
" worlds:",
" " + worldName + ":",
" generator: uSkyBlock",
"- Make sure no other plugin loads the world before",
" uSkyBlock created it.",
"Already generated terrain can be removed with",
"'/usb chunk regen <x> <z> <radius>'.",
"============================================================");
}

private void warnWrongGenerator(@NotNull World world) {
wrongGeneratorWarning(world.getName()).forEach(logger::severe);
}

/**
* Gets the skyblock island {@link World}. Creates and/or imports the world if necessary.
*
Expand All @@ -222,11 +270,9 @@ public synchronized World getWorld() {
String worldName = runtimeConfigs.current().general().worldName();
skyBlockWorld = Bukkit.getWorld(worldName);
ChunkGenerator skyGenerator = getOverworldGenerator();
ChunkGenerator worldGenerator = skyBlockWorld != null ? skyBlockWorld.getGenerator() : null;
if (skyBlockWorld == null
|| skyBlockWorld.canGenerateStructures()
|| worldGenerator == null
|| !worldGenerator.getClass().getName().equals(skyGenerator.getClass().getName())) {
|| !hasExpectedGenerator(skyBlockWorld, skyGenerator)) {
skyBlockWorld = WorldCreator
.name(worldName)
.type(WorldType.NORMAL)
Expand All @@ -236,6 +282,11 @@ public synchronized World getWorld() {
.createWorld();
skyBlockWorld.save();
}
if (!hasExpectedGenerator(skyBlockWorld, skyGenerator)) {
// createWorld() returns an already-loaded world unchanged, so the generator
// could not be swapped; tell the admin how to fix the setup.
warnWrongGenerator(skyBlockWorld);
}

scheduleOverworldSetup(skyBlockWorld);
}
Expand All @@ -260,11 +311,9 @@ public synchronized World getNetherWorld() {
String worldName = runtimeConfig.general().worldName();
skyBlockNetherWorld = Bukkit.getWorld(worldName + "_nether");
ChunkGenerator skyGenerator = getNetherGenerator();
ChunkGenerator worldGenerator = skyBlockNetherWorld != null ? skyBlockNetherWorld.getGenerator() : null;
if (skyBlockNetherWorld == null
|| skyBlockNetherWorld.canGenerateStructures()
|| worldGenerator == null
|| !worldGenerator.getClass().getName().equals(skyGenerator.getClass().getName())) {
|| !hasExpectedGenerator(skyBlockNetherWorld, skyGenerator)) {
skyBlockNetherWorld = WorldCreator
.name(worldName + "_nether")
.type(WorldType.NORMAL)
Expand All @@ -274,6 +323,11 @@ public synchronized World getNetherWorld() {
.createWorld();
skyBlockNetherWorld.save();
}
if (!hasExpectedGenerator(skyBlockNetherWorld, skyGenerator)) {
// createWorld() returns an already-loaded world unchanged, so the generator
// could not be swapped; tell the admin how to fix the setup.
warnWrongGenerator(skyBlockNetherWorld);
}

scheduleNetherSetup(skyBlockNetherWorld);
}
Expand Down Expand Up @@ -312,13 +366,15 @@ public boolean isSkyNether(@Nullable World world) {

private void scheduleOverworldSetup(@NotNull World world) {
scheduler.sync(() -> {
bukkitYmlGeneratorMapping.ensureMapping(world.getName());
hookManager.getWorldHook().ifPresent(hook -> hook.registerOverworld(world));
setupWorld(world, runtimeConfigs.current().island().height());
});
}

private void scheduleNetherSetup(@NotNull World world) {
scheduler.sync(() -> {
bukkitYmlGeneratorMapping.ensureMapping(world.getName());
hookManager.getWorldHook().ifPresent(hook -> hook.registerNetherworld(world));
hookManager.getInventorySyncHook().ifPresent(hook -> hook.linkNetherInventory(getWorld(), world));
setupWorld(world, runtimeConfigs.current().island().height() / 2);
Expand Down
Loading