Merge pull request #3149 from Multiverse/ben/mv5/single-biome

Add support for creating worlds with single biome
This commit is contained in:
Ben Woo 2025-01-05 23:10:13 +08:00 committed by GitHub
commit 5dddfbebc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 254 additions and 8 deletions

View File

@ -11,9 +11,13 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.collect.Lists;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@ -59,6 +63,15 @@ class CreateCommand extends CoreCommand {
.addAlias("-a")
.build());
private final CommandValueFlag<Biome> BIOME_FLAG = flag(CommandValueFlag.builder("--biome", Biome.class)
.addAlias("-b")
.completion(input -> Lists.newArrayList(Registry.BIOME).stream()
.filter(biome -> biome !=Biome.CUSTOM)
.map(biome -> biome.getKey().getKey())
.toList())
.context(biomeStr -> Registry.BIOME.get(NamespacedKey.minecraft(biomeStr)))
.build());
@Inject
CreateCommand(
@NotNull MVCommandManager commandManager,
@ -73,7 +86,7 @@ class CreateCommand extends CoreCommand {
@Subcommand("create")
@CommandPermission("multiverse.core.create")
@CommandCompletion("@empty @environments @flags:groupName=mvcreatecommand")
@Syntax("<name> <environment> --seed [seed] --generator [generator[:id]] --world-type [worldtype] --adjust-spawn --no-structures")
@Syntax("<name> <environment> [--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn --no-structures --biome <biome>]")
@Description("{@@mv-core.create.description}")
void onCreateCommand(
MVCommandIssuer issuer,
@ -87,7 +100,7 @@ class CreateCommand extends CoreCommand {
World.Environment environment,
@Optional
@Syntax("--seed [seed] --generator [generator[:id]] --world-type [worldtype] --adjust-spawn --no-structures")
@Syntax("[--seed <seed> --generator <generator[:id]> --world-type <worldtype> --adjust-spawn --no-structures --biome <biome>]")
@Description("{@@mv-core.create.flags.description}")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
@ -102,14 +115,21 @@ class CreateCommand extends CoreCommand {
"{worldType}", parsedFlags.flagValue(WORLD_TYPE_FLAG, WorldType.NORMAL).name());
issuer.sendInfo(MVCorei18n.CREATE_PROPERTIES_ADJUSTSPAWN,
"{adjustSpawn}", String.valueOf(!parsedFlags.hasFlag(NO_ADJUST_SPAWN_FLAG)));
issuer.sendInfo(MVCorei18n.CREATE_PROPERTIES_GENERATOR,
"{generator}", parsedFlags.flagValue(GENERATOR_FLAG, ""));
if (parsedFlags.hasFlag(BIOME_FLAG)) {
issuer.sendInfo(MVCorei18n.CREATE_PROPERTIES_BIOME,
"{biome}", parsedFlags.flagValue(BIOME_FLAG, Biome.CUSTOM).name());
}
if (parsedFlags.hasFlag(GENERATOR_FLAG)) {
issuer.sendInfo(MVCorei18n.CREATE_PROPERTIES_GENERATOR,
"{generator}", parsedFlags.flagValue(GENERATOR_FLAG));
}
issuer.sendInfo(MVCorei18n.CREATE_PROPERTIES_STRUCTURES,
"{structures}", String.valueOf(!parsedFlags.hasFlag(NO_STRUCTURES_FLAG)));
issuer.sendInfo(MVCorei18n.CREATE_LOADING);
worldManager.createWorld(CreateWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(BIOME_FLAG, Biome.CUSTOM))
.environment(environment)
.seed(parsedFlags.flagValue(SEED_FLAG))
.worldType(parsedFlags.flagValue(WORLD_TYPE_FLAG, WorldType.NORMAL))

View File

@ -9,8 +9,12 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.collect.Lists;
import jakarta.inject.Inject;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@ -42,6 +46,16 @@ class ImportCommand extends CoreCommand {
.addAlias("-n")
.build());
private final CommandValueFlag<Biome> BIOME_FLAG = flag(CommandValueFlag.builder("--biome", Biome.class)
.addAlias("-b")
//todo: Implement some default completions or smt to reduce duplication
.completion(input -> Lists.newArrayList(Registry.BIOME).stream()
.filter(biome -> biome !=Biome.CUSTOM)
.map(biome -> biome.getKey().getKey())
.toList())
.context(biomeStr -> Registry.BIOME.get(NamespacedKey.minecraft(biomeStr)))
.build());
@Inject
ImportCommand(
@NotNull MVCommandManager commandManager,
@ -56,7 +70,7 @@ class ImportCommand extends CoreCommand {
@Subcommand("import")
@CommandPermission("multiverse.core.import")
@CommandCompletion("@mvworlds:scope=potential @environments @flags:groupName=mvimportcommand")
@Syntax("<name> <env> --generator [generator[:id]] --adjust-spawn")
@Syntax("<name> <env> [--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Description("{@@mv-core.import.description}")
void onImportCommand(
MVCommandIssuer issuer,
@ -71,13 +85,14 @@ class ImportCommand extends CoreCommand {
World.Environment environment,
@Optional
@Syntax("--generator [generator[:id]] --adjust-spawn")
@Syntax("[--generator <generator[:id]> --adjust-spawn --biome <biome>]")
@Description("{@@mv-core.import.other.description}")
String[] flags) {
ParsedCommandFlags parsedFlags = parseFlags(flags);
issuer.sendInfo(MVCorei18n.IMPORT_IMPORTING, "{world}", worldName);
worldManager.importWorld(ImportWorldOptions.worldName(worldName)
.biome(parsedFlags.flagValue(BIOME_FLAG, Biome.CUSTOM))
.environment(environment)
.generator(parsedFlags.flagValue(GENERATOR_FLAG, String.class))
.useSpawnAdjust(!parsedFlags.hasFlag(NO_ADJUST_SPAWN_FLAG)))

View File

@ -117,13 +117,14 @@ class InfoCommand extends CoreCommand {
outMap.put("World Name", world.getName());
outMap.put("World Alias", world.getAlias());
outMap.put("World UID", world.getUID().toString());
outMap.put("Game Mode: ", world.getGameMode().toString());
outMap.put("Game Mode", world.getGameMode().toString());
outMap.put("Difficulty", world.getDifficulty().toString());
outMap.put("Spawn Location", locationManipulation.strCoords(world.getSpawnLocation()));
outMap.put("Seed", String.valueOf(world.getSeed()));
getEntryFeeInfo(outMap, world); // Entry fee/reward
outMap.put("Respawn World", world.getRespawnWorldName());
outMap.put("World Type", world.getWorldType().get().toString());
outMap.put("Biome", world.getBiome() == null ? "@vanilla" : world.getBiome().getKey().getKey());
outMap.put("Generator", world.getGenerator());
outMap.put("Generate Structures", world.canGenerateStructures().get().toString());
outMap.put("World Scale", String.valueOf(world.getScale()));

View File

@ -13,7 +13,10 @@ import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.Syntax;
import com.dumptruckman.minecraft.util.Logging;
import com.google.common.collect.Lists;
import jakarta.inject.Inject;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
@ -114,6 +117,7 @@ class RegenCommand extends CoreCommand {
LoadedMultiverseWorld world,
ParsedCommandFlags parsedFlags,
List<Player> worldPlayers) {
//todo: Change biome on regen
RegenWorldOptions regenWorldOptions = RegenWorldOptions.world(world)
.randomSeed(parsedFlags.hasFlag(SEED_FLAG))
.seed(parsedFlags.flagValue(SEED_FLAG))

View File

@ -37,6 +37,7 @@ public enum MVCorei18n implements MessageKeyProvider {
CREATE_PROPERTIES_SEED,
CREATE_PROPERTIES_WORLDTYPE,
CREATE_PROPERTIES_ADJUSTSPAWN,
CREATE_PROPERTIES_BIOME,
CREATE_PROPERTIES_GENERATOR,
CREATE_PROPERTIES_STRUCTURES,
CREATE_LOADING,

View File

@ -10,6 +10,7 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.bukkit.entity.Player;
import org.bukkit.generator.BiomeProvider;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.api.BlockSafety;
@ -47,6 +48,10 @@ public class LoadedMultiverseWorld extends MultiverseWorld {
private void setupWorldConfig(World world) {
worldConfig.setMVWorld(this);
worldConfig.load();
BiomeProvider biomeProvider = world.getBiomeProvider();
if (biomeProvider instanceof SingleBiomeProvider singleBiomeProvider) {
worldConfig.setBiome(singleBiomeProvider.getBiome());
}
worldConfig.setEnvironment(world.getEnvironment());
worldConfig.setSeed(world.getSeed());
}

View File

@ -10,6 +10,8 @@ import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle;
@ -195,6 +197,16 @@ public class MultiverseWorld {
return worldConfig.getBedRespawn();
}
/**
* Gets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @return The biome used for this world
*/
public @Nullable Biome getBiome() {
return worldConfig.getBiome();
}
/**
* Sets whether or not a player who dies in this world will respawn in their
* bed or follow the normal respawn pattern.

View File

@ -0,0 +1,37 @@
package org.mvplugins.multiverse.core.world;
import org.bukkit.WorldCreator;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.WorldInfo;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Helps create a world with only 1 type of Biome specified. Used in {@link WorldCreator#biomeProvider(BiomeProvider)}
*/
public class SingleBiomeProvider extends BiomeProvider {
private final Biome biome;
private final List<Biome> biomes;
public SingleBiomeProvider(Biome biome) {
this.biome = biome;
this.biomes = List.of(biome);
}
@Override
public @NotNull Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z) {
return this.biome;
}
@Override
public @NotNull List<Biome> getBiomes(@NotNull WorldInfo worldInfo) {
return this.biomes;
}
public Biome getBiome() {
return biome;
}
}

View File

@ -20,6 +20,8 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.block.Biome;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -199,6 +201,7 @@ public class WorldManager {
CreateWorldOptions options) {
String parsedGenerator = parseGenerator(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
.biomeProvider(createSingleBiomeProvider(options.biome()))
.environment(options.environment())
.generateStructures(options.generateStructures())
.generator(parsedGenerator)
@ -246,6 +249,7 @@ public class WorldManager {
ImportWorldOptions options) {
String parsedGenerator = parseGenerator(options.worldName(), options.generator());
WorldCreator worldCreator = WorldCreator.name(options.worldName())
.biomeProvider(createSingleBiomeProvider(options.biome()))
.environment(options.environment())
.generator(parsedGenerator);
return createBukkitWorld(worldCreator).fold(
@ -337,6 +341,7 @@ public class WorldManager {
private Attempt<LoadedMultiverseWorld, LoadFailureReason> doLoadWorld(@NotNull MultiverseWorld mvWorld) {
return createBukkitWorld(WorldCreator.name(mvWorld.getName())
.biomeProvider(createSingleBiomeProvider(mvWorld.getBiome()))
.environment(mvWorld.getEnvironment())
.generator(Strings.isNullOrEmpty(mvWorld.getGenerator()) ? null : mvWorld.getGenerator())
.seed(mvWorld.getSeed())).fold(
@ -355,6 +360,18 @@ public class WorldManager {
});
}
/**
* Creates a single biome provider for the specified biome.
* @param biome The biome
* @return The single biome provider or null if biome is null or custom
*/
private @Nullable BiomeProvider createSingleBiomeProvider(@Nullable Biome biome) {
if (biome == null || biome == Biome.CUSTOM) {
return null;
}
return new SingleBiomeProvider(biome);
}
/**
* Unloads an existing multiverse world. It will still remain as an unloaded world.
*
@ -516,6 +533,7 @@ public class WorldManager {
.mapAttempt(validatedOptions -> {
ImportWorldOptions importWorldOptions = ImportWorldOptions
.worldName(validatedOptions.newWorldName())
.biome(validatedOptions.world().getBiome())
.environment(validatedOptions.world().getEnvironment())
.generator(validatedOptions.world().getGenerator());
return importWorld(importWorldOptions).transform(CloneFailureReason.IMPORT_FAILED);

View File

@ -0,0 +1,33 @@
package org.mvplugins.multiverse.core.world.config;
import org.bukkit.block.Biome;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
public class BiomeSerializer implements NodeSerializer<Biome> {
static final String VANILLA_BIOME_BEHAVIOUR = "@vanilla";
@Override
public Biome deserialize(Object object, Class<Biome> type) {
if (object instanceof Biome) {
return (Biome) object;
}
try {
String biomeStr = String.valueOf(object);
if (biomeStr.equalsIgnoreCase(VANILLA_BIOME_BEHAVIOUR)) {
return null;
}
return Biome.valueOf(biomeStr.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public Object serialize(Biome biome, Class<Biome> type) {
if (biome == null || biome == Biome.CUSTOM) {
return VANILLA_BIOME_BEHAVIOUR;
}
return biome.name().toLowerCase();
}
}

View File

@ -10,6 +10,7 @@ import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.configuration.ConfigurationSection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -165,6 +166,14 @@ public final class WorldConfig {
return configHandle.set(configNodes.AUTO_LOAD, autoLoad);
}
public Biome getBiome() {
return configHandle.get(configNodes.BIOME);
}
public Try<Void> setBiome(Biome biome) {
return configHandle.set(configNodes.BIOME, biome);
}
public boolean getBedRespawn() {
return configHandle.get(configNodes.BED_RESPAWN);
}

View File

@ -7,6 +7,7 @@ import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.mvplugins.multiverse.core.MultiverseCore;
@ -95,6 +96,12 @@ public class WorldConfigNodes {
.defaultValue(true)
.build());
final ConfigNode<Biome> BIOME = node(ConfigNode.builder("biome", Biome.class)
.defaultValue(Biome.CUSTOM)
.name(null)
.serializer(new BiomeSerializer())
.build());
final ConfigNode<Difficulty> DIFFICULTY = node(ConfigNode.builder("difficulty", Difficulty.class)
.defaultValue(Difficulty.NORMAL)
.onSetValue((oldValue, newValue) -> {

View File

@ -4,6 +4,7 @@ import java.util.Random;
import org.bukkit.World;
import org.bukkit.WorldType;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -23,6 +24,7 @@ public class CreateWorldOptions {
}
private final String worldName;
private Biome biome;
private World.Environment environment = World.Environment.NORMAL;
private boolean generateStructures = true;
private String generator = null;
@ -49,6 +51,28 @@ public class CreateWorldOptions {
return worldName;
}
/**
* Sets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @param biome The biome used for this world
* @return This {@link CreateWorldOptions} instance.
*/
public @NotNull CreateWorldOptions biome(@Nullable Biome biome) {
this.biome = biome;
return this;
}
/**
* Gets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @return The biome used for this world
*/
public @NotNull Biome biome() {
return biome;
}
/**
* Sets the environment of the world to create.
*

View File

@ -1,6 +1,7 @@
package org.mvplugins.multiverse.core.world.options;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -20,6 +21,7 @@ public class ImportWorldOptions {
}
private final String worldName;
private Biome biome;
private World.Environment environment = World.Environment.NORMAL;
private String generator = null;
private boolean useSpawnAdjust = true;
@ -37,6 +39,28 @@ public class ImportWorldOptions {
return worldName;
}
/**
* Sets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @param biome The biome used for this world
* @return This {@link ImportWorldOptions} instance.
*/
public @NotNull ImportWorldOptions biome(@Nullable Biome biome) {
this.biome = biome;
return this;
}
/**
* Gets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @return The biome used for this world
*/
public @NotNull Biome biome() {
return biome;
}
/**
* Sets the environment of the world to create.
*

View File

@ -2,6 +2,7 @@ package org.mvplugins.multiverse.core.world.options;
import java.util.Random;
import org.bukkit.block.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -23,9 +24,9 @@ public final class RegenWorldOptions implements KeepWorldSettingsOptions {
}
private final LoadedMultiverseWorld world;
private Biome biome;
private boolean keepGameRule = true;
private boolean keepWorldConfig = true;
private boolean keepWorldBorder = true;
private boolean randomSeed = false;
private long seed = Long.MIN_VALUE;
@ -43,6 +44,28 @@ public final class RegenWorldOptions implements KeepWorldSettingsOptions {
return world;
}
/**
* Sets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @param biome The biome used for this world
* @return This {@link RegenWorldOptions} instance.
*/
public @NotNull RegenWorldOptions biome(@Nullable Biome biome) {
this.biome = biome;
return this;
}
/**
* Gets the single biome used for this world. This may be null, in which case the biome from the generator will be used.
* If no generator is specified, the "natural" biome behaviour for this environment will be used.
*
* @return The biome used for this world
*/
public @NotNull Biome biome() {
return biome;
}
/**
* Sets whether to keep the game rule of the world during regeneration.
*

View File

@ -39,6 +39,7 @@ mv-core.create.properties.environment=- Environment: &f{environment}
mv-core.create.properties.seed=- Seed: &f{seed}
mv-core.create.properties.worldtype=- World Type: &f{worldType}
mv-core.create.properties.adjustspawn=- Adjust Spawn: &f{adjustSpawn}
mv-core.create.properties.biome=- Biome: &f{biome}
mv-core.create.properties.generator=- Generator: &f{generator}
mv-core.create.properties.structures=- Structures: &f{structures}
mv-core.create.loading=Creating world...

View File

@ -27,4 +27,6 @@ class CreateCommandTest : AbstractWorldCommandTest() {
assertEquals(WorldType.FLAT, world.get().worldType.get())
assertFalse(world.get().canGenerateStructures().get())
}
//todo: Fix mockbukkit getBiomeProvider then added test on single biome world creation
}

View File

@ -13,6 +13,7 @@ world:
currency: '@vault-economy'
environment: NORMAL
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true
@ -51,6 +52,7 @@ world_nether:
currency: '@vault-economy'
environment: NETHER
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true

View File

@ -13,6 +13,7 @@ world_nether:
currency: '@vault-economy'
environment: NETHER
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true

View File

@ -13,6 +13,7 @@ world_the_end:
currency: '@vault-economy'
environment: THE_END
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true
@ -56,6 +57,7 @@ world:
currency: '@vault-economy'
environment: NORMAL
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true

View File

@ -13,6 +13,7 @@ world:
currency: '@vault-economy'
environment: NORMAL
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true
@ -51,6 +52,7 @@ world_nether:
currency: '@vault-economy'
environment: NETHER
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true
@ -82,6 +84,7 @@ newworld:
auto-heal: true
auto-load: true
bed-respawn: true
biome: '@vanilla'
difficulty: NORMAL
entry-fee:
enabled: false

View File

@ -13,6 +13,7 @@ world:
currency: @vault-economy
environment: NORMAL
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true
@ -56,6 +57,7 @@ world_nether:
currency: @vault-economy
environment: NETHER
gamemode: SURVIVAL
biome: '@vanilla'
generator: ''
hidden: false
hunger: true