diff --git a/api/build.gradle.kts b/api/build.gradle.kts index b15e8d74..c63ae0d3 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -11,7 +11,7 @@ java { } group = "net.thenextlvl.worlds" -version = "1.2.5" +version = "2.0.0" repositories { mavenCentral() diff --git a/api/src/main/java/net/thenextlvl/worlds/api/WorldsProvider.java b/api/src/main/java/net/thenextlvl/worlds/api/WorldsProvider.java new file mode 100644 index 00000000..d712ca8d --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/WorldsProvider.java @@ -0,0 +1,13 @@ +package net.thenextlvl.worlds.api; + +import net.thenextlvl.worlds.api.link.LinkController; +import net.thenextlvl.worlds.api.view.GeneratorView; +import net.thenextlvl.worlds.api.view.LevelView; + +public interface WorldsProvider { + GeneratorView generatorView(); + + LevelView levelView(); + + LinkController linkController(); +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/event/WorldDeleteEvent.java b/api/src/main/java/net/thenextlvl/worlds/api/event/WorldDeleteEvent.java new file mode 100644 index 00000000..fb947221 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/event/WorldDeleteEvent.java @@ -0,0 +1,19 @@ +package net.thenextlvl.worlds.api.event; + +import lombok.Getter; +import org.bukkit.World; +import org.bukkit.event.HandlerList; +import org.bukkit.event.world.WorldEvent; + +public class WorldDeleteEvent extends WorldEvent { + private static final @Getter HandlerList handlerList = new HandlerList(); + + public WorldDeleteEvent(World world) { + super(world, false); + } + + @Override + public HandlerList getHandlers() { + return getHandlerList(); + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/event/WorldRegenerateEvent.java b/api/src/main/java/net/thenextlvl/worlds/api/event/WorldRegenerateEvent.java new file mode 100644 index 00000000..a759da78 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/event/WorldRegenerateEvent.java @@ -0,0 +1,9 @@ +package net.thenextlvl.worlds.api.event; + +import org.bukkit.World; + +public class WorldRegenerateEvent extends WorldDeleteEvent { + public WorldRegenerateEvent(World world) { + super(world); + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/package-info.java b/api/src/main/java/net/thenextlvl/worlds/api/event/package-info.java similarity index 87% rename from api/src/main/java/net/thenextlvl/worlds/preset/adapter/package-info.java rename to api/src/main/java/net/thenextlvl/worlds/api/event/package-info.java index f390eb1c..74d0fc35 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/package-info.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/event/package-info.java @@ -2,7 +2,7 @@ @FieldsAreNotNullByDefault @ParametersAreNotNullByDefault @MethodsReturnNotNullByDefault -package net.thenextlvl.worlds.preset.adapter; +package net.thenextlvl.worlds.api.event; import core.annotation.FieldsAreNotNullByDefault; import core.annotation.MethodsReturnNotNullByDefault; diff --git a/api/src/main/java/net/thenextlvl/worlds/api/link/LinkController.java b/api/src/main/java/net/thenextlvl/worlds/api/link/LinkController.java new file mode 100644 index 00000000..15a46020 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/link/LinkController.java @@ -0,0 +1,21 @@ +package net.thenextlvl.worlds.api.link; + +import org.bukkit.NamespacedKey; +import org.bukkit.PortalType; +import org.bukkit.World; + +import java.util.Optional; + +public interface LinkController { + Optional getTarget(World world, PortalType type); + + Optional getTarget(World world, Relative relative); + + Optional getTarget(World world, World.Environment type); + + boolean canLink(World source, World destination); + + boolean link(World source, World destination); + + boolean unlink(World source, Relative relative); +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/link/Relative.java b/api/src/main/java/net/thenextlvl/worlds/api/link/Relative.java new file mode 100644 index 00000000..7ce61c2d --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/link/Relative.java @@ -0,0 +1,38 @@ +package net.thenextlvl.worlds.api.link; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.World; + +import java.util.Arrays; +import java.util.Optional; + +@Getter +@RequiredArgsConstructor +@Accessors(fluent = true) +public enum Relative implements Keyed { + OVERWORLD(new NamespacedKey("relative", "overworld")), + NETHER(new NamespacedKey("relative", "nether")), + THE_END(new NamespacedKey("relative", "the_end")); + + private final NamespacedKey key; + + public static Optional valueOf(Key key) { + return Arrays.stream(values()) + .filter(value -> value.key().equals(key)) + .findAny(); + } + + public static Optional valueOf(World.Environment environment) { + return Optional.ofNullable(switch (environment) { + case NORMAL -> Relative.OVERWORLD; + case NETHER -> Relative.NETHER; + case THE_END -> Relative.THE_END; + default -> null; + }); + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/model/LevelExtras.java b/api/src/main/java/net/thenextlvl/worlds/api/model/LevelExtras.java new file mode 100644 index 00000000..4af0c735 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/model/LevelExtras.java @@ -0,0 +1,10 @@ +package net.thenextlvl.worlds.api.model; + +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.Nullable; + +public record LevelExtras( + @Nullable NamespacedKey key, + boolean enabled +) { +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/model/WorldPreset.java b/api/src/main/java/net/thenextlvl/worlds/api/model/WorldPreset.java new file mode 100644 index 00000000..11df817b --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/model/WorldPreset.java @@ -0,0 +1,14 @@ +package net.thenextlvl.worlds.api.model; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; + +public record WorldPreset(Key key) implements Keyed { + public static final WorldPreset AMPLIFIED = new WorldPreset(Key.key("minecraft", "amplified")); + public static final WorldPreset CHECKERBOARD = new WorldPreset(Key.key("minecraft", "checkerboard")); + public static final WorldPreset DEBUG = new WorldPreset(Key.key("minecraft", "debug")); + public static final WorldPreset FLAT = new WorldPreset(Key.key("minecraft", "flat")); + public static final WorldPreset LARGE_BIOMES = new WorldPreset(Key.key("minecraft", "large_biomes")); + public static final WorldPreset NORMAL = new WorldPreset(Key.key("minecraft", "noise")); + public static final WorldPreset SINGLE_BIOME = new WorldPreset(Key.key("minecraft", "fixed")); +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/model/package-info.java b/api/src/main/java/net/thenextlvl/worlds/api/model/package-info.java new file mode 100644 index 00000000..4c2724f6 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/model/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@ParametersAreNotNullByDefault +@MethodsReturnNotNullByDefault +package net.thenextlvl.worlds.api.model; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/Biome.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/Biome.java similarity index 80% rename from api/src/main/java/net/thenextlvl/worlds/preset/Biome.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/Biome.java index a81d2bbc..d3af2bfe 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/Biome.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/Biome.java @@ -1,17 +1,16 @@ -package net.thenextlvl.worlds.preset; +package net.thenextlvl.worlds.api.preset; import com.google.common.base.Preconditions; public record Biome(String provider, String biome) { + Biome(org.bukkit.block.Biome biome) { + this(biome.key().namespace(), biome.key().value()); + } public static Biome minecraft(String biome) { return new Biome("minecraft", biome); } - public static Biome bukkit(org.bukkit.block.Biome biome) { - return new Biome(biome.key().namespace(), biome.key().value()); - } - public static Biome literal(String string) { var split = string.split(":", 2); Preconditions.checkArgument(split.length == 2, "Not a valid biome: " + string); diff --git a/api/src/main/java/net/thenextlvl/worlds/api/preset/Layer.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/Layer.java new file mode 100644 index 00000000..a08db8f3 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/Layer.java @@ -0,0 +1,14 @@ +package net.thenextlvl.worlds.api.preset; + +import org.bukkit.Material; + +public record Layer(String block, int height) { + Layer(Material material, int height) { + this(material.key().asString(), height); + } + + @Override + public String toString() { + return height() != 1 ? height() + "*" + block() : block(); + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/Preset.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/Preset.java similarity index 73% rename from api/src/main/java/net/thenextlvl/worlds/preset/Preset.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/Preset.java index 90c45dbe..044a8ce0 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/Preset.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/Preset.java @@ -1,4 +1,4 @@ -package net.thenextlvl.worlds.preset; +package net.thenextlvl.worlds.api.preset; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -10,13 +10,13 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -import net.thenextlvl.worlds.preset.adapter.BiomeTypeAdapter; -import net.thenextlvl.worlds.preset.adapter.StructureTypeAdapter; +import net.thenextlvl.worlds.api.preset.adapter.BiomeTypeAdapter; +import net.thenextlvl.worlds.api.preset.adapter.StructureTypeAdapter; import org.bukkit.Material; import java.io.File; -import java.util.ArrayList; -import java.util.List; +import java.util.LinkedHashSet; +import java.util.stream.Collectors; @Getter @Setter @@ -27,9 +27,9 @@ public class Preset { private boolean features; private boolean decoration; - private final List layers = new ArrayList<>(); + private LinkedHashSet layers = new LinkedHashSet<>(); @SerializedName("structure_overrides") - private final List structures = new ArrayList<>(); + private LinkedHashSet structures = new LinkedHashSet<>(); /** * Add a layer to the preset @@ -66,6 +66,15 @@ public boolean saveToFile(File file, boolean force) { return true; } + /** + * Serialize this preset into a json object. + * + * @return the serialized preset as a JsonObject + */ + public JsonObject serialize() { + return gson.toJsonTree(this).getAsJsonObject(); + } + private static final Gson gson = new GsonBuilder() .registerTypeAdapter(Structure.class, new StructureTypeAdapter()) .registerTypeAdapter(Material.class, MaterialAdapter.NotNull.INSTANCE) @@ -73,16 +82,6 @@ public boolean saveToFile(File file, boolean force) { .setPrettyPrinting() .create(); - /** - * Serialize a preset into a json object - * - * @param preset the preset to serialize - * @return the serialized preset - */ - public static JsonObject serialize(Preset preset) { - return gson.toJsonTree(preset).getAsJsonObject(); - } - /** * Deserialize a json object into a preset * @@ -92,4 +91,12 @@ public static JsonObject serialize(Preset preset) { public static Preset deserialize(JsonObject object) { return gson.fromJson(object, Preset.class); } + + @Override + public String toString() { + var layers = layers().stream() + .map(Layer::toString) + .collect(Collectors.joining(",")); + return layers + ";" + biome(); + } } diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/PresetFile.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/PresetFile.java similarity index 95% rename from api/src/main/java/net/thenextlvl/worlds/preset/PresetFile.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/PresetFile.java index 6cf95e89..0d06bf6a 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/PresetFile.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/PresetFile.java @@ -1,4 +1,4 @@ -package net.thenextlvl.worlds.preset; +package net.thenextlvl.worlds.api.preset; import com.google.gson.Gson; import com.google.gson.JsonObject; diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/Presets.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/Presets.java similarity index 98% rename from api/src/main/java/net/thenextlvl/worlds/preset/Presets.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/Presets.java index 5d6409cc..e078245d 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/Presets.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/Presets.java @@ -1,4 +1,4 @@ -package net.thenextlvl.worlds.preset; +package net.thenextlvl.worlds.api.preset; import org.bukkit.Material; diff --git a/api/src/main/java/net/thenextlvl/worlds/api/preset/Structure.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/Structure.java new file mode 100644 index 00000000..420bd33a --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/Structure.java @@ -0,0 +1,16 @@ +package net.thenextlvl.worlds.api.preset; + +public record Structure(String structure) { + Structure(org.bukkit.generator.structure.Structure structure) { + this(structure.key().asString()); + } + + public static Structure minecraft(String structure) { + return new Structure("minecraft:" + structure); + } + + @Override + public String toString() { + return structure(); + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/BiomeTypeAdapter.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/BiomeTypeAdapter.java similarity index 84% rename from api/src/main/java/net/thenextlvl/worlds/preset/adapter/BiomeTypeAdapter.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/BiomeTypeAdapter.java index 1f00d618..1c6fbf13 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/BiomeTypeAdapter.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/BiomeTypeAdapter.java @@ -1,7 +1,7 @@ -package net.thenextlvl.worlds.preset.adapter; +package net.thenextlvl.worlds.api.preset.adapter; import com.google.gson.*; -import net.thenextlvl.worlds.preset.Biome; +import net.thenextlvl.worlds.api.preset.Biome; import java.lang.reflect.Type; diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/StructureTypeAdapter.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/StructureTypeAdapter.java similarity index 76% rename from api/src/main/java/net/thenextlvl/worlds/preset/adapter/StructureTypeAdapter.java rename to api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/StructureTypeAdapter.java index 4a061a38..93322700 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/adapter/StructureTypeAdapter.java +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/StructureTypeAdapter.java @@ -1,14 +1,14 @@ -package net.thenextlvl.worlds.preset.adapter; +package net.thenextlvl.worlds.api.preset.adapter; import com.google.gson.*; -import net.thenextlvl.worlds.preset.Structure; +import net.thenextlvl.worlds.api.preset.Structure; import java.lang.reflect.Type; public class StructureTypeAdapter implements JsonSerializer, JsonDeserializer { @Override public Structure deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException { - return Structure.literal(element.getAsString()); + return new Structure(element.getAsString()); } @Override diff --git a/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/package-info.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/package-info.java new file mode 100644 index 00000000..462923d6 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/adapter/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@ParametersAreNotNullByDefault +@MethodsReturnNotNullByDefault +package net.thenextlvl.worlds.api.preset.adapter; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/api/src/main/java/net/thenextlvl/worlds/api/preset/package-info.java b/api/src/main/java/net/thenextlvl/worlds/api/preset/package-info.java new file mode 100644 index 00000000..afced07a --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/preset/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@ParametersAreNotNullByDefault +@MethodsReturnNotNullByDefault +package net.thenextlvl.worlds.api.preset; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/api/src/main/java/net/thenextlvl/worlds/api/view/GeneratorView.java b/api/src/main/java/net/thenextlvl/worlds/api/view/GeneratorView.java new file mode 100644 index 00000000..52a9f31b --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/view/GeneratorView.java @@ -0,0 +1,11 @@ +package net.thenextlvl.worlds.api.view; + +import org.bukkit.plugin.Plugin; + +public interface GeneratorView { + boolean hasGenerator(Plugin plugin); + + boolean hasChunkGenerator(Class clazz); + + boolean hasBiomeProvider(Class clazz); +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/view/LevelView.java b/api/src/main/java/net/thenextlvl/worlds/api/view/LevelView.java new file mode 100644 index 00000000..b5696b54 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/view/LevelView.java @@ -0,0 +1,59 @@ +package net.thenextlvl.worlds.api.view; + +import core.nbt.file.NBTFile; +import core.nbt.tag.CompoundTag; +import net.thenextlvl.worlds.api.model.LevelExtras; +import net.thenextlvl.worlds.api.model.WorldPreset; +import net.thenextlvl.worlds.api.preset.Preset; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public interface LevelView { + @Nullable World loadLevel(File level); + + @Nullable World loadLevel(File level, @Nullable NamespacedKey key, Predicate> predicate); + + @Nullable World loadLevel(File level, Predicate> predicate); + + @Nullable World loadLevel(File level, World.Environment environment); + + @Nullable World loadLevel(File level, World.Environment environment, @Nullable NamespacedKey key, Predicate> predicate); + + @Nullable World loadLevel(File level, World.Environment environment, Predicate> predicate); + + NBTFile getLevelDataFile(File level); + + Optional getExtras(CompoundTag data); + + Optional getFlatPreset(CompoundTag generator); + + Optional getGeneratorSettings(CompoundTag generator); + + Optional getGeneratorType(CompoundTag generator); + + Optional getWorldPreset(CompoundTag generator); + + Stream listLevels(); + + String getDimension(CompoundTag dimensions, World.Environment environment); + + World.Environment getEnvironment(File level); + + boolean canLoad(File level); + + boolean hasEndDimension(File level); + + boolean hasNetherDimension(File level); + + boolean isLevel(File file); + + void saveLevel(World world, boolean flush); + + void saveLevelData(World world, boolean async); +} diff --git a/api/src/main/java/net/thenextlvl/worlds/api/view/package-info.java b/api/src/main/java/net/thenextlvl/worlds/api/view/package-info.java new file mode 100644 index 00000000..132f3fdc --- /dev/null +++ b/api/src/main/java/net/thenextlvl/worlds/api/view/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@ParametersAreNotNullByDefault +@MethodsReturnNotNullByDefault +package net.thenextlvl.worlds.api.view; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/api/src/main/java/net/thenextlvl/worlds/image/DeletionType.java b/api/src/main/java/net/thenextlvl/worlds/image/DeletionType.java deleted file mode 100644 index 7a2aeb20..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/image/DeletionType.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.thenextlvl.worlds.image; - -public enum DeletionType { - WORLD, WORLD_AND_IMAGE; - - public boolean keepImage() { - return equals(WORLD); - } -} diff --git a/api/src/main/java/net/thenextlvl/worlds/image/Generator.java b/api/src/main/java/net/thenextlvl/worlds/image/Generator.java deleted file mode 100644 index e9020573..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/image/Generator.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.thenextlvl.worlds.image; - -import org.bukkit.World; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.PluginClassLoader; -import org.jetbrains.annotations.Nullable; - -public record Generator(String plugin, @Nullable String id) { - - @Nullable - public static Generator of(World world) { - var plugin = getGeneratorPlugin(world); - return plugin != null ? new Generator(plugin.getName(), null) : null; - } - - @Nullable - @SuppressWarnings("UnstableApiUsage") - public static Plugin getGeneratorPlugin(World world) { - if (world.getGenerator() == null) return null; - var loader = world.getGenerator().getClass().getClassLoader(); - if (!(loader instanceof PluginClassLoader pluginLoader)) return null; - return pluginLoader.getPlugin(); - } - - public static boolean hasChunkGenerator(Class clazz) { - try { - return clazz.getMethod("getDefaultWorldGenerator", String.class, String.class).getDeclaringClass().equals(clazz); - } catch (NoSuchMethodException e) { - return false; - } - } - - public static boolean hasBiomeProvider(Class clazz) { - try { - return clazz.getMethod("getDefaultBiomeProvider", String.class, String.class).getDeclaringClass().equals(clazz); - } catch (NoSuchMethodException e) { - return false; - } - } - - @Override - public String toString() { - return id() != null ? plugin() + ":" + id() : plugin(); - } -} diff --git a/api/src/main/java/net/thenextlvl/worlds/image/Image.java b/api/src/main/java/net/thenextlvl/worlds/image/Image.java deleted file mode 100644 index 7d559b3c..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/image/Image.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.thenextlvl.worlds.image; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.bukkit.World; - -public interface Image { - - Image save(); - - World getWorld(); - - @Deprecated - WorldImage getWorldImage(); - - boolean unload(); - - boolean canUnload(); - - boolean canDelete(); - - DeleteResult delete(boolean keepImage, boolean keepWorld, boolean schedule); - - DeleteResult deleteNow(boolean keepImage, boolean keepWorld); - - DeleteResult scheduleDeletion(boolean keepImage, boolean keepWorld); - - @Getter - @RequiredArgsConstructor - enum DeleteResult { - WORLD_DELETE_SCHEDULED("world.delete.scheduled"), - WORLD_DELETE_ILLEGAL("world.delete.disallowed"), - WORLD_DELETE_NOTHING("world.delete.nothing"), - WORLD_DELETE_FAILED("world.delete.failed"), - WORLD_DELETED("world.delete.success"), - - IMAGE_DELETE_FAILED("image.delete.failed"), - - WORLD_UNLOAD_FAILED("world.unload.failed"), - WORLD_UNLOADED("world.unload.success"); - - private final String message; - } -} diff --git a/api/src/main/java/net/thenextlvl/worlds/image/ImageProvider.java b/api/src/main/java/net/thenextlvl/worlds/image/ImageProvider.java deleted file mode 100644 index b4cc23b1..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/image/ImageProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.thenextlvl.worlds.image; - -import org.bukkit.World; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.List; - -public interface ImageProvider { - - @Nullable - @Deprecated - Image load(@Nullable WorldImage image); - - @Nullable - Image get(World world); - - void register(Image image); - - Image getOrDefault(World world); - - List findImageFiles(); - - List findWorldFiles(); - - @Deprecated - List findImages(); - - @Deprecated - WorldImage createWorldImage(); - - @Deprecated - WorldImage of(World world); - - @Nullable - @Deprecated - WorldImage of(File file); -} diff --git a/api/src/main/java/net/thenextlvl/worlds/image/WorldImage.java b/api/src/main/java/net/thenextlvl/worlds/image/WorldImage.java deleted file mode 100644 index e5dc3df0..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/image/WorldImage.java +++ /dev/null @@ -1,209 +0,0 @@ -package net.thenextlvl.worlds.image; - -import com.google.gson.JsonObject; -import org.bukkit.NamespacedKey; -import org.bukkit.World; -import org.bukkit.WorldType; -import org.jetbrains.annotations.Nullable; - -@Deprecated -public interface WorldImage { - /** - * Retrieves the name of the WorldImage. - * - * @return The name of the WorldImage. - */ - String name(); - - /** - * Sets the name of the WorldImage. - * - * @param name The new name for the WorldImage. - * @return The updated WorldImage instance. - */ - WorldImage name(String name); - - /** - * Retrieves the NamespacedKey associated with the WorldImage. - * - * @return The NamespacedKey associated with the WorldImage. - */ - NamespacedKey key(); - - /** - * Sets the NamespacedKey associated with the WorldImage. - * - * @param key The NamespacedKey to set. - * @return The updated WorldImage instance. - */ - WorldImage key(NamespacedKey key); - - /** - * Retrieves the settings associated with the WorldImage. - * - * @return The settings associated with the WorldImage, or null if no settings are specified. - */ - @Nullable - JsonObject settings(); - - /** - * Set the settings associated with the WorldImage. - * - * @param object The JsonObject representing the settings to set. - * @return The updated WorldImage instance. - */ - WorldImage settings(@Nullable JsonObject object); - - /** - * Retrieves the Generator associated with the WorldImage. - * - * @return The Generator associated with the WorldImage, or null if no Generator is specified. - */ - @Nullable - Generator generator(); - - /** - * Sets the world generator for the WorldImage. - * - * @param generator The Generator to set as the world generator. - * @return The updated WorldImage instance. - */ - WorldImage generator(@Nullable Generator generator); - - /** - * Retrieves the DeletionType associated with the WorldImage. - * - * @return The DeletionType associated with the WorldImage, or null if no DeletionType is specified. - */ - @Nullable - DeletionType deletionType(); - - /** - * Retrieves the DeletionType associated with the WorldImage. - * - * @param deletionType The DeletionType to associate with the WorldImage. - * @return The updated WorldImage instance. - */ - WorldImage deletionType(@Nullable DeletionType deletionType); - - - /** - * Retrieves the environment of the WorldImage. - * - * @return The environment of the WorldImage. - */ - World.Environment environment(); - - /** - * Retrieves the environment of the WorldImage. - * - * @param environment The environment to set for the WorldImage. - * @return The updated WorldImage instance. - */ - WorldImage environment(World.Environment environment); - - /** - * Retrieves the world type of the WorldImage. - * - * @return The world type of the WorldImage. - */ - WorldType worldType(); - - /** - * Sets the world type of the WorldImage. - * - * @param worldType The world type to set for the WorldImage. - * @return The updated WorldImage instance. - */ - WorldImage worldType(WorldType worldType); - - /** - * Returns whether the WorldImage has auto save enabled or not. - * - * @return true if auto save is enabled, false otherwise. - */ - boolean autoSave(); - - /** - * Enables or disables the auto save feature for the WorldImage. - * - * @param autoSave true to enable auto save, false to disable auto save. - * @return The updated WorldImage instance. - */ - WorldImage autoSave(boolean autoSave); - - /** - * Checks if structures are generated in the world. - * - * @return true if structures are generated, false otherwise. - */ - @Deprecated - boolean generateStructures(); - - /** - * Generates structures in the world based on the given flag. - * - * @param generateStructures Specifies whether structures should be generated in the world. - * Set to true to generate structures, false otherwise. - * @return The updated WorldImage instance. - */ - @Deprecated - WorldImage generateStructures(boolean generateStructures); - - /** - * Checks if the WorldImage is set to hardcore mode. - * - * @return true if the WorldImage is set to hardcore mode, false otherwise. - */ - @Deprecated - boolean hardcore(); - - /** - * Sets the hardcore mode for the WorldImage. - * - * @param hardcore true to enable hardcore mode, false to disable hardcore mode. - * @return The updated WorldImage instance. - */ - @Deprecated - WorldImage hardcore(boolean hardcore); - - /** - * Returns whether the WorldImage is set to load on start. - * - * @return true if the WorldImage is set to load on start, false otherwise. - */ - boolean loadOnStart(); - - /** - * Sets whether the WorldImage should be loaded on start. - * - * @param loadOnStart true if the WorldImage should be loaded on start, false otherwise. - * @return The updated WorldImage instance. - */ - WorldImage loadOnStart(boolean loadOnStart); - - /** - * Retrieves the seed of the WorldImage. - * - * @return The seed of the WorldImage. - */ - @Deprecated - long seed(); - - /** - * Sets the seed of the WorldImage. - * - * @param seed The seed to set for the WorldImage. - * @return The updated WorldImage instance. - */ - @Deprecated - WorldImage seed(long seed); - - /** - * Builds and returns a World object based on the configuration of the WorldImage. - * - * @return The built World object, or null if the build fails. - */ - @Nullable - World build(); -} diff --git a/api/src/main/java/net/thenextlvl/worlds/link/Link.java b/api/src/main/java/net/thenextlvl/worlds/link/Link.java deleted file mode 100644 index 4bc992f8..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/link/Link.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.thenextlvl.worlds.link; - -import com.google.gson.annotations.SerializedName; -import org.bukkit.PortalType; -import org.bukkit.World; - -public record Link( - @SerializedName("portal") PortalType portalType, - @SerializedName("source") World source, - @SerializedName("destination") World destination -) { - @Override - public String toString() { - return portalType.name().toLowerCase() + ": " + source.getName() + " -> " + destination.getName(); - } -} diff --git a/api/src/main/java/net/thenextlvl/worlds/link/LinkRegistry.java b/api/src/main/java/net/thenextlvl/worlds/link/LinkRegistry.java deleted file mode 100644 index 4c98bbb1..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/link/LinkRegistry.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.thenextlvl.worlds.link; - -import org.bukkit.World; - -import java.util.stream.Stream; - -public interface LinkRegistry { - - Stream getLinks(); - - boolean isRegistered(Link link); - - boolean register(Link link); - - boolean unregister(Link link); - - boolean unregisterAll(World world); -} diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/Layer.java b/api/src/main/java/net/thenextlvl/worlds/preset/Layer.java deleted file mode 100644 index e5ff601b..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/preset/Layer.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.thenextlvl.worlds.preset; - -import org.bukkit.Material; -import org.jetbrains.annotations.Range; - -public record Layer(Material block, @Range(from = 1, to = Long.MAX_VALUE) int height) { -} diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/Structure.java b/api/src/main/java/net/thenextlvl/worlds/preset/Structure.java deleted file mode 100644 index 741c10c7..00000000 --- a/api/src/main/java/net/thenextlvl/worlds/preset/Structure.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.thenextlvl.worlds.preset; - -import com.google.common.base.Preconditions; - -public record Structure(String provider, String structure) { - - public static Structure minecraft(String structure) { - return new Structure("minecraft", structure); - } - - public static Structure bukkit(org.bukkit.generator.structure.Structure structure) { - return new Structure(structure.key().namespace(), structure.key().value()); - } - - public static Structure literal(String string) { - var split = string.split(":", 2); - Preconditions.checkArgument(split.length == 2, "Not a valid structure: " + string); - Preconditions.checkArgument(!split[0].isBlank(), "Structure provider cannot be empty"); - Preconditions.checkArgument(!split[1].isBlank(), "Structure name cannot be empty"); - return new Structure(split[0], split[1]); - } - - @Override - public String toString() { - return provider() + ":" + structure(); - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..2c352119 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20db9ad5..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 77aa4dff..c3a63b4b 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -4,14 +4,18 @@ import net.minecrell.pluginyml.bukkit.BukkitPluginDescription plugins { id("java") id("io.papermc.hangar-publish-plugin") version "0.1.2" + id("io.papermc.paperweight.userdev") version "1.7.1" id("net.minecrell.plugin-yml.paper") version "0.6.0" id("io.github.goooler.shadow") version "8.1.8" id("com.modrinth.minotaur") version "2.+" } java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + toolchain.languageVersion = JavaLanguageVersion.of(21) +} + +tasks.compileJava { + options.release.set(21) } group = project(":api").group @@ -19,57 +23,64 @@ version = project(":api").version repositories { mavenCentral() + maven("https://jitpack.io") maven("https://repo.thenextlvl.net/releases") maven("https://repo.thenextlvl.net/snapshots") maven("https://repo.papermc.io/repository/maven-public/") } dependencies { + paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT") + compileOnly("org.projectlombok:lombok:1.18.34") + compileOnly("net.thenextlvl.core:annotations:2.0.1") - compileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT") implementation("org.bstats:bstats-bukkit:3.0.2") - implementation("org.incendo:cloud-paper:2.0.0-SNAPSHOT") - implementation("org.incendo:cloud-minecraft-extras:2.0.0-SNAPSHOT") implementation(project(":api")) implementation("net.thenextlvl.core:nbt:1.4.2") implementation("net.thenextlvl.core:files:1.0.5") implementation("net.thenextlvl.core:i18n:1.0.19") - implementation("net.thenextlvl.core:paper:1.3.5") + implementation("net.thenextlvl.core:paper:1.4.1") implementation("net.thenextlvl.core:adapters:1.0.9") annotationProcessor("org.projectlombok:lombok:1.18.34") } - tasks.shadowJar { relocate("org.bstats", "net.thenextlvl.worlds.bstats") archiveBaseName.set("worlds") - // minimize() // breaks cloud } paper { name = "Worlds" - main = "net.thenextlvl.worlds.Worlds" + main = "net.thenextlvl.worlds.WorldsPlugin" apiVersion = "1.20" description = "Create, delete and manage your worlds" - load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD + load = BukkitPluginDescription.PluginLoadOrder.STARTUP website = "https://thenextlvl.net" authors = listOf("NonSwag") permissions { - register("worlds.commands.world") { + register("worlds.commands.admin") { this.children = listOf( - "worlds.command.world.create", - "worlds.command.world.delete", - "worlds.command.world.export", - "worlds.command.world.import", - "worlds.command.world.info", - "worlds.command.world.list", - "worlds.command.world.setspawn", - "worlds.command.world.teleport" + "worlds.command.clone", + "worlds.command.create", + "worlds.command.delete", + "worlds.command.import", + "worlds.command.info", + "worlds.command.link", + "worlds.command.list", + "worlds.command.load", + "worlds.command.save", + "worlds.command.save-all", + "worlds.command.save-off", + "worlds.command.save-on", + "worlds.command.setspawn", + "worlds.command.spawn", + "worlds.command.teleport", + "worlds.command.unload", ) } register("worlds.commands.link") { @@ -79,6 +90,64 @@ paper { "worlds.command.link.list" ) } + register("worlds.command.link.create") { + this.children = listOf("worlds.command.link") + } + register("worlds.command.link.delete") { + this.children = listOf("worlds.command.link") + } + register("worlds.command.link.list") { + this.children = listOf("worlds.command.link") + } + + register("worlds.command.link") { + this.children = listOf("worlds.command") + } + register("worlds.command.clone") { + this.children = listOf("worlds.command") + } + register("worlds.command.create") { + this.children = listOf("worlds.command") + } + register("worlds.command.delete") { + this.children = listOf("worlds.command") + } + register("worlds.command.import") { + this.children = listOf("worlds.command") + } + register("worlds.command.info") { + this.children = listOf("worlds.command") + } + register("worlds.command.list") { + this.children = listOf("worlds.command") + } + register("worlds.command.load") { + this.children = listOf("worlds.command") + } + register("worlds.command.save") { + this.children = listOf("worlds.command") + } + register("worlds.command.save-all") { + this.children = listOf("worlds.command") + } + register("worlds.command.save-off") { + this.children = listOf("worlds.command") + } + register("worlds.command.save-on") { + this.children = listOf("worlds.command") + } + register("worlds.command.setspawn") { + this.children = listOf("worlds.command") + } + register("worlds.command.spawn") { + this.children = listOf("worlds.command") + } + register("worlds.command.teleport") { + this.children = listOf("worlds.command") + } + register("worlds.command.unload") { + this.children = listOf("worlds.command") + } } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/Worlds.java b/plugin/src/main/java/net/thenextlvl/worlds/WorldsPlugin.java similarity index 54% rename from plugin/src/main/java/net/thenextlvl/worlds/Worlds.java rename to plugin/src/main/java/net/thenextlvl/worlds/WorldsPlugin.java index e451733b..a408e7cb 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/Worlds.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/WorldsPlugin.java @@ -8,31 +8,40 @@ import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.thenextlvl.worlds.api.WorldsProvider; +import net.thenextlvl.worlds.api.link.LinkController; +import net.thenextlvl.worlds.api.preset.Presets; +import net.thenextlvl.worlds.api.view.GeneratorView; +import net.thenextlvl.worlds.api.view.LevelView; import net.thenextlvl.worlds.command.WorldCommand; -import net.thenextlvl.worlds.image.CraftImageProvider; -import net.thenextlvl.worlds.image.WorldImage; -import net.thenextlvl.worlds.link.CraftLinkRegistry; -import net.thenextlvl.worlds.link.LinkRegistry; +import net.thenextlvl.worlds.controller.WorldLinkController; import net.thenextlvl.worlds.listener.PortalListener; -import net.thenextlvl.worlds.listener.WorldListener; -import net.thenextlvl.worlds.preset.Presets; +import net.thenextlvl.worlds.listener.ServerListener; +import net.thenextlvl.worlds.version.PluginVersionChecker; +import net.thenextlvl.worlds.view.PaperLevelView; +import net.thenextlvl.worlds.view.PluginGeneratorView; import org.bstats.bukkit.Metrics; -import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.util.Locale; -import java.util.Objects; + +import static org.bukkit.persistence.PersistentDataType.BOOLEAN; +import static org.bukkit.persistence.PersistentDataType.STRING; @Getter @Accessors(fluent = true) @FieldsAreNotNullByDefault @ParametersAreNotNullByDefault -public class Worlds extends JavaPlugin { - private final CraftImageProvider imageProvider = new CraftImageProvider(this); - private final CraftLinkRegistry linkRegistry = new CraftLinkRegistry(this); +public class WorldsPlugin extends JavaPlugin implements WorldsProvider { + private final GeneratorView generatorView = new PluginGeneratorView(); + private final LevelView levelView = new PaperLevelView(this); + + private final LinkController linkController = new WorldLinkController(this); private final File presetsFolder = new File(getDataFolder(), "presets"); private final File translations = new File(getDataFolder(), "translations"); @@ -44,49 +53,51 @@ public class Worlds extends JavaPlugin { .miniMessage(bundle -> MiniMessage.builder().tags(TagResolver.resolver( TagResolver.standard(), Placeholder.component("prefix", bundle.component(Locale.US, "prefix")) - )).build());; + )).build()); + private final PluginVersionChecker versionChecker = new PluginVersionChecker(this); private final Metrics metrics = new Metrics(this, 19652); @Override public void onLoad() { - Bukkit.getServicesManager().register(LinkRegistry.class, linkRegistry(), this, ServicePriority.Highest); - - saveDefaultPresets(); + if (!presetsFolder().isDirectory()) saveDefaultPresets(); + versionChecker().checkVersion(); + registerServices(); } @Override public void onEnable() { - imageProvider().findImages().stream() - .filter(WorldImage::loadOnStart) - .forEach(imageProvider()::load); - linkRegistry().loadLinks(); registerListeners(); registerCommands(); } @Override public void onDisable() { - Bukkit.getWorlds().stream() - .map(imageProvider()::get) - .filter(Objects::nonNull) - .forEach(image -> { - var deletionType = image.getWorldImage().deletionType(); - if (deletionType != null) { - image.getWorld().getPlayers().forEach(player -> player.kick(Bukkit.shutdownMessage())); - image.deleteNow(deletionType.keepImage(), false); - } else if (!image.getWorldImage().autoSave()) { - image.getWorld().getPlayers().forEach(player -> player.kick(Bukkit.shutdownMessage())); - image.unload(); - } - }); - linkRegistry().saveLinks(); metrics().shutdown(); + unloadWorlds(); + } + + private void unloadWorlds() { + getServer().getWorlds().stream().filter(world -> !world.isAutoSave()).forEach(world -> { + world.getPlayers().forEach(player -> player.kick(getServer().shutdownMessage())); + getServer().unloadWorld(world, false); + }); + } + + public void persistWorld(World world, boolean enabled) { + if (world.key().asString().equals("minecraft:overworld")) return; + var container = world.getPersistentDataContainer(); + container.set(new NamespacedKey("worlds", "world_key"), STRING, world.getKey().asString()); + container.set(new NamespacedKey("worlds", "enabled"), BOOLEAN, enabled); + } + + private void registerServices() { + getServer().getServicesManager().register(WorldsProvider.class, this, this, ServicePriority.Highest); } private void registerListeners() { - Bukkit.getPluginManager().registerEvents(new PortalListener(this), this); - Bukkit.getPluginManager().registerEvents(new WorldListener(this), this); + getServer().getPluginManager().registerEvents(new PortalListener(this), this); + getServer().getPluginManager().registerEvents(new ServerListener(this), this); } private void registerCommands() { diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/CustomSyntaxFormatter.java b/plugin/src/main/java/net/thenextlvl/worlds/command/CustomSyntaxFormatter.java deleted file mode 100644 index 10401363..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/CustomSyntaxFormatter.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.thenextlvl.worlds.command; - -import core.annotation.MethodsReturnNotNullByDefault; -import core.annotation.ParametersAreNotNullByDefault; -import org.incendo.cloud.CommandManager; -import org.incendo.cloud.syntax.StandardCommandSyntaxFormatter; - -@MethodsReturnNotNullByDefault -@ParametersAreNotNullByDefault -public class CustomSyntaxFormatter extends StandardCommandSyntaxFormatter { - public CustomSyntaxFormatter(CommandManager manager) { - super(manager); - } - - @Override - protected FormattingInstance createInstance() { - return new FormattingInstance() { - @Override - public String optionalPrefix() { - return "("; - } - - @Override - public String optionalSuffix() { - return ")"; - } - - @Override - public String requiredPrefix() { - return "["; - } - - @Override - public String requiredSuffix() { - return "]"; - } - - @Override - public void appendPipe() { - appendName(" | "); - } - }; - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCloneCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCloneCommand.java new file mode 100644 index 00000000..df889062 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCloneCommand.java @@ -0,0 +1,102 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldCloneCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("clone") + .requires(source -> source.getSender().hasPermission("worlds.command.clone")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.argument("key", ArgumentTypes.namespacedKey()) + .then(Commands.literal("template") + .executes(context -> clone(context, false))) + .executes(context -> clone(context, true)))); + } + + private int clone(CommandContext context, boolean full) { + var world = context.getArgument("world", World.class); + var key = context.getArgument("key", NamespacedKey.class); + var clone = clone(world, key, full); + + var placeholder = Placeholder.parsed("world", world.key().asString()); + var message = clone != null ? "world.clone.success" : "world.clone.failed"; + + if (clone != null && context.getSource().getSender() instanceof Player player) + player.teleportAsync(clone.getSpawnLocation(), COMMAND); + + plugin.bundle().sendMessage(context.getSource().getSender(), message, placeholder); + return clone != null ? Command.SINGLE_SUCCESS : 0; + } + + private @Nullable World clone(World world, NamespacedKey key, boolean full) { + if (plugin.getServer().getWorld(key) != null) return null; + if (plugin.getServer().getWorld(key.getKey()) != null) return null; + if (new File(plugin.getServer().getWorldContainer(), key.getKey()).isDirectory()) return null; + if (full) copy(world, new File(plugin.getServer().getWorldContainer(), key.getKey())); + return new WorldCreator(key.getKey(), key).copy(world).createWorld(); + } + + private void copy(World world, File destination) { + var files = world.getWorldFolder().listFiles(this::shouldCopy); + if (files == null) return; + for (File file : files) copy(file, new File(destination, file.getName())); + } + + private boolean shouldCopy(File file, String name) { + if (name.equals("advancements") && file.isDirectory()) return false; + if (name.equals("datapacks") && file.isDirectory()) return false; + if (name.equals("playerdata") && file.isDirectory()) return false; + if (name.equals("session.lock")) return false; + if (name.equals("stats") && file.isDirectory()) return false; + return !name.equals("uid.dat"); + } + + private void copy(File source, File destination) { + if (source.isDirectory()) copyDirectory(source, destination); + else copyFile(source, destination); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void copyDirectory(File source, File destination) { + if (!destination.exists()) destination.mkdirs(); + var list = source.listFiles(); + if (list == null) return; + for (var file : list) copy(file, new File(destination, file.getName())); + } + + private void copyFile(File source, File destination) { + try (var in = new FileInputStream(source); + var out = new FileOutputStream(destination)) { + int length; + var buf = new byte[1024]; + while ((length = in.read(buf)) > 0) out.write(buf, 0, length); + } catch (IOException ignored) { + } + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCommand.java index ebcda41f..9b3c78b5 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCommand.java @@ -1,69 +1,37 @@ package net.thenextlvl.worlds.command; -import com.google.gson.JsonParseException; -import io.papermc.paper.command.brigadier.CommandSourceStack; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import org.incendo.cloud.bukkit.parser.PlayerParser; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.exception.ArgumentParseException; -import org.incendo.cloud.exception.InvalidCommandSenderException; -import org.incendo.cloud.exception.InvalidSyntaxException; -import org.incendo.cloud.exception.NoPermissionException; -import org.incendo.cloud.execution.ExecutionCoordinator; -import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler; -import org.incendo.cloud.paper.PaperCommandManager; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +@RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") public class WorldCommand { - private final PaperCommandManager commandManager; - private final Worlds plugin; - - public WorldCommand(Worlds plugin) { - this.commandManager = PaperCommandManager.builder() - .executionCoordinator(ExecutionCoordinator.simpleCoordinator()) - .buildOnEnable(plugin); - MinecraftExceptionHandler.create(CommandSourceStack::getSender) - .handler(InvalidSyntaxException.class, (formatter, context) -> { - var syntax = context.exception().correctSyntax() - .replace("[", "[").replace("]", "]") - .replace("(", "(").replace(")", ")") - .replace("|", "|").replace("--", "--"); - return plugin.bundle().deserialize(" /" + syntax); - }) - .handler(InvalidCommandSenderException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "command.sender")) - .handler(NoPermissionException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "command.permission", - Placeholder.parsed("permission", context.exception().missingPermission().permissionString()))) - .handler(ArgumentParseException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "command.argument")) - .handler(PlayerParser.PlayerParseException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "player.unknown", - Placeholder.parsed("player", context.exception().input()))) - .handler(WorldParser.WorldParseException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "world.unknown", - Placeholder.parsed("world", context.exception().input()))) - .handler(JsonParseException.class, (formatter, context) -> - plugin.bundle().component(context.context().sender().getSender(), "world.preset.invalid")) - .defaultCommandExecutionHandler() - .registerTo(commandManager); - commandManager.commandSyntaxFormatter(new CustomSyntaxFormatter<>(commandManager)); - this.plugin = plugin; - } + private final WorldsPlugin plugin; public void register() { - var world = commandManager.commandBuilder("world"); - commandManager.command(new WorldCreateCommand(plugin, world).create()); - commandManager.command(new WorldDeleteCommand(plugin, world).create()); - commandManager.command(new WorldExportCommand(plugin, world).create()); - commandManager.command(new WorldImportCommand(plugin, world).create()); - commandManager.command(new WorldInfoCommand(plugin, world).create()); - commandManager.command(new WorldLinkCommand.Create(plugin, world).create()); - commandManager.command(new WorldLinkCommand.Delete(plugin, world).create()); - commandManager.command(new WorldLinkCommand.List(plugin, world).create()); - commandManager.command(new WorldListCommand(plugin, world).create()); - commandManager.command(new WorldSetSpawnCommand(plugin, world).create()); - commandManager.command(new WorldTeleportCommand(plugin, world).create()); + var command = Commands.literal("world") + .requires(source -> source.getSender().hasPermission("worlds.command")) + .then(new WorldCloneCommand(plugin).create()) + .then(new WorldCreateCommand(plugin).create()) + .then(new WorldDeleteCommand(plugin).create()) + .then(new WorldImportCommand(plugin).create()) + .then(new WorldInfoCommand(plugin).create()) + .then(new WorldLinkCommand(plugin).create()) + .then(new WorldListCommand(plugin).create()) + .then(new WorldLoadCommand(plugin).create()) + .then(new WorldRegenerateCommand(plugin).create()) + .then(new WorldSaveAllCommand(plugin).create()) + .then(new WorldSaveCommand(plugin).create()) + .then(new WorldSaveOffCommand(plugin).create()) + .then(new WorldSaveOnCommand(plugin).create()) + .then(new WorldSetSpawnCommand(plugin).create()) + .then(new WorldSpawnCommand(plugin).create()) + .then(new WorldTeleportCommand(plugin).create()) + .then(new WorldUnloadCommand(plugin).create()) + .build(); + plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> + event.registrar().register(command))); } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCreateCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCreateCommand.java index 224d775a..6a262d66 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCreateCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldCreateCommand.java @@ -1,236 +1,131 @@ package net.thenextlvl.worlds.command; -import com.google.gson.JsonObject; -import core.io.IO; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; import lombok.RequiredArgsConstructor; -import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import net.thenextlvl.worlds.image.DeletionType; -import net.thenextlvl.worlds.image.Generator; -import net.thenextlvl.worlds.preset.Preset; -import net.thenextlvl.worlds.preset.PresetFile; -import net.thenextlvl.worlds.preset.Presets; -import net.thenextlvl.worlds.util.WorldReader; -import org.bukkit.Bukkit; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.preset.Preset; +import net.thenextlvl.worlds.command.argument.*; import org.bukkit.NamespacedKey; import org.bukkit.World; -import org.bukkit.World.Environment; +import org.bukkit.WorldCreator; import org.bukkit.WorldType; import org.bukkit.entity.Entity; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.generator.WorldInfo; -import org.bukkit.plugin.Plugin; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.NamespacedKeyParser; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.component.TypedCommandComponent; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.minecraft.extras.RichDescription; -import org.incendo.cloud.parser.flag.CommandFlag; -import org.incendo.cloud.parser.standard.BooleanParser; -import org.incendo.cloud.parser.standard.EnumParser; -import org.incendo.cloud.parser.standard.StringParser; -import org.incendo.cloud.suggestion.Suggestion; -import org.incendo.cloud.suggestion.SuggestionProvider; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import org.jetbrains.annotations.Nullable; + import java.util.concurrent.ThreadLocalRandom; +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldCreateCommand { - private final Worlds plugin; - private final Command.Builder builder; - - Command.Builder create() { - return builder.literal("create") - .permission("worlds.command.world.create") - .required("name", StringParser.stringParser(), - SuggestionProvider.blocking((context, input) -> - plugin.imageProvider().findWorldFiles().stream() - .map(File::getName) - .filter(s -> Bukkit.getWorld(s) == null) - .map(Suggestion::suggestion) - .toList())) - .flag(CommandFlag.builder("type").withAliases("t") - .withDescription(RichDescription.of(Component.text("The world type"))) - .withComponent(TypedCommandComponent.ofType(WorldType.class, "type") - .parser(EnumParser.enumParser(WorldType.class)))) - .flag(CommandFlag.builder("environment").withAliases("e") - .withDescription(RichDescription.of(Component.text("The environment"))) - .withComponent(TypedCommandComponent.ofType(Environment.class, "environment") - .parser(EnumParser.enumParser(Environment.class)))) - .flag(CommandFlag.builder("generator").withAliases("g") - .withDescription(RichDescription.of(Component.text("The generator plugin"))) - .withComponent(TypedCommandComponent.ofType(String.class, "generator") - .parser(StringParser.greedyFlagYieldingStringParser()) - .suggestionProvider(SuggestionProvider.blocking((context, input) -> - Arrays.stream(Bukkit.getPluginManager().getPlugins()) - .filter(plugin -> Generator.hasChunkGenerator(plugin.getClass()) - || Generator.hasBiomeProvider(plugin.getClass())) - .map(Plugin::getName) - .map(Suggestion::suggestion) - .toList())))) - .flag(CommandFlag.builder("base").withAliases("b") - .withDescription(RichDescription.of(Component.text("The world to clone"))) - .withComponent(TypedCommandComponent.ofType(World.class, "world") - .parser(WorldParser.worldParser()))) - .flag(CommandFlag.builder("preset") - .withDescription(RichDescription.of(Component.text("The preset to use"))) - .withComponent(TypedCommandComponent.ofType(String.class, "preset") - .parser(StringParser.greedyFlagYieldingStringParser()) - .suggestionProvider(SuggestionProvider.blocking((context, input) -> - PresetFile.findPresets(plugin.presetsFolder()).stream() - .map(file -> file.getName().substring(0, file.getName().length() - 5)) - .map(name -> name.contains(" ") ? "\"" + name + "\"" : name) - .map(Suggestion::suggestion) - .toList())))) - .flag(CommandFlag.builder("deletion").withAliases("d") - .withDescription(RichDescription.of(Component.text("What to do with the world on shutdown"))) - .withComponent(TypedCommandComponent.ofType(DeletionType.class, "deletion") - .parser(EnumParser.enumParser(DeletionType.class)))) - .flag(CommandFlag.builder("identifier").withAliases("i") - .withDescription(RichDescription.of(Component.text("The identifier of the world generator"))) - .withComponent(TypedCommandComponent.ofType(String.class, "identifier") - .parser(StringParser.greedyFlagYieldingStringParser()))) - .flag(CommandFlag.builder("key") - .withDescription(RichDescription.of(Component.text("The namespaced key"))) - .withComponent(TypedCommandComponent.ofType(NamespacedKey.class, "key") - .parser(NamespacedKeyParser.namespacedKeyParser(true)))) - .flag(CommandFlag.builder("seed").withAliases("s") - .withDescription(RichDescription.of(Component.text("The seed"))) - .withComponent(TypedCommandComponent.ofType(String.class, "seed") - .parser(StringParser.greedyFlagYieldingStringParser()))) - .flag(CommandFlag.builder("auto-save") - .withDescription(RichDescription.of(Component.text("Whether the world should auto-save"))) - .withComponent(TypedCommandComponent.ofType(boolean.class, "auto-save") - .parser(BooleanParser.booleanParser()))) - .flag(CommandFlag.builder("structures") - .withDescription(RichDescription.of(Component.text("Whether structures should generate"))) - .withComponent(TypedCommandComponent.ofType(boolean.class, "structures") - .parser(BooleanParser.booleanParser()))) - .flag(CommandFlag.builder("hardcore") - .withDescription(RichDescription.of(Component.text("Whether hardcore is enabled")))) - .flag(CommandFlag.builder("load-manual") - .withDescription(RichDescription.of(Component.text("Whether the world must be loaded manual on startup")))) - .handler(this::execute); + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("create") + .requires(source -> source.getSender().hasPermission("worlds.command.create")) + .then(Commands.argument("key", ArgumentTypes.namespacedKey()) + .then(Commands.literal("generator") + .then(Commands.argument("generator", new GeneratorArgument(plugin)) + .executes(context -> createGenerator(context, World.Environment.NORMAL, + true, ThreadLocalRandom.current().nextLong())) + .then(tree(this::createGenerator)))) + .then(Commands.literal("preset") + .then(Commands.argument("preset", new WorldPresetArgument(plugin)) + .executes(context -> createPreset(context, World.Environment.NORMAL, + true, ThreadLocalRandom.current().nextLong())) + .then(tree(this::createPreset)))) + .then(Commands.literal("type") + .then(Commands.argument("type", new WorldTypeArgument(plugin)) + .executes(context -> createType(context, World.Environment.NORMAL, + true, ThreadLocalRandom.current().nextLong())) + .then(tree(this::createType)))) + .executes(context -> create(context, World.Environment.NORMAL, true, + ThreadLocalRandom.current().nextLong(), WorldType.NORMAL, null, null))); } - @SuppressWarnings("deprecation") - private void execute(CommandContext context) { - var name = context.get("name"); - - var sender = context.sender().getSender(); + private RequiredArgumentBuilder tree(Creator creator) { + return Commands.argument("dimension", new DimensionArgument(plugin)) + .then(Commands.argument("structures", BoolArgumentType.bool()) + .then(Commands.argument("seed", new SeedArgument()) + .executes(context -> { + var environment = context.getArgument("dimension", World.Environment.class); + var structures = context.getArgument("structures", boolean.class); + var seed = context.getArgument("seed", long.class); + return creator.create(context, environment, structures, seed); + })) + .executes(context -> { + var environment = context.getArgument("dimension", World.Environment.class); + var structures = context.getArgument("structures", boolean.class); + return creator.create(context, environment, structures, + ThreadLocalRandom.current().nextLong()); + })) + .executes(context -> { + var environment = context.getArgument("dimension", World.Environment.class); + return creator.create(context, environment, true, ThreadLocalRandom.current().nextLong()); + }); + } - if (Bukkit.getWorld(name) != null) { - plugin.bundle().sendMessage(sender, "world.known", Placeholder.parsed("world", name)); - return; + private int create(CommandContext context, World.Environment environment, boolean structures, + long seed, WorldType worldType, @Nullable Preset preset, @Nullable GeneratorArgument.Generator generator) { + var key = context.getArgument("key", NamespacedKey.class); + var name = key.getKey(); + var creator = new WorldCreator(name, key) + .environment(environment) + .generateStructures(structures) + .seed(seed) + .type(worldType); + + if (preset != null) creator.generatorSettings(preset.serialize().toString()); + + if (generator != null) { + creator.generator(generator.plugin().getDefaultWorldGenerator(name, generator.id())); + creator.biomeProvider(generator.plugin().getDefaultBiomeProvider(name, generator.id())); } - var worldReader = new WorldReader(name); + var world = plugin.getServer().getWorld(creator.key()) == null + && plugin.getServer().getWorld(name) == null + ? creator.createWorld() : null; - var environment = context.flags().getValue("environment").orElse(Environment.NORMAL); - if (environment.equals(Environment.CUSTOM)) { - plugin.bundle().sendMessage(sender, "environment.custom"); - return; - } + var message = world != null ? "world.create.success" : "world.create.failed"; + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("world", world != null ? world.key().asString() : key.asString())); - var base = context.flags().getValue("base"); - var key = context.flags().getValue("key").orElse(new NamespacedKey("worlds", name.toLowerCase())); - var type = context.flags().getValue("type") - .orElse(context.flags().contains("preset") ? WorldType.FLAT : WorldType.NORMAL); - var identifier = context.flags().getValue("identifier", null); - var generator = context.flags().getValue("generator") - .map(string -> new Generator(string, identifier)).orElse(null); - var seed = context.flags().getValue("seed").map(s -> { - try { - return Long.parseLong(s); - } catch (NumberFormatException e) { - return s.hashCode(); - } - }).orElse(worldReader.seed().orElse(base.map(WorldInfo::getSeed) - .orElse(ThreadLocalRandom.current().nextLong()))) - .longValue(); - var deletion = context.flags().getValue("deletion", null); - var loadManual = context.flags().contains("load-manual"); - var structures = context.flags().getValue("structures") - .orElse(worldReader.generateStructures().orElse(base.map(World::canGenerateStructures).orElse(true))); - var autoSave = context.flags().getValue("auto-save") - .orElse(base.map(World::isAutoSave).orElse(true)); - var hardcore = context.flags().contains("hardcore") || worldReader.hardcore() - .orElse(base.map(World::isHardcore).orElse(false)); - var preset = context.flags().getValue("preset", null); - JsonObject settings = null; - - if (preset != null && generator != null) { - plugin.bundle().sendMessage(sender, "command.flag.combination", - Placeholder.parsed("flag-1", "generator"), - Placeholder.parsed("flag-2", "preset")); - return; - } else if (preset != null && !Objects.equals(type, WorldType.FLAT)) { - plugin.bundle().sendMessage(sender, "world.preset.flat"); - return; - } else if (preset != null) { - final var fileName = preset + ".json"; - var match = PresetFile.of(IO.of(plugin.presetsFolder(), fileName)); - if (match != null) settings = match.settings(); - } else if (type.equals(WorldType.FLAT)) { - settings = Preset.serialize(Presets.CLASSIC_FLAT); + if (world != null && context.getSource().getSender() instanceof Entity entity) + entity.teleportAsync(world.getSpawnLocation(), COMMAND); + + if (world != null) { + plugin.persistWorld(world, true); + plugin.levelView().saveLevelData(world, true); } - base.ifPresent(world -> { - var placeholder = Placeholder.parsed("world", world.getName()); - if (copy(world.getWorldFolder(), new File(Bukkit.getWorldContainer(), name))) - plugin.bundle().sendMessage(sender, "world.clone.success", placeholder); - else plugin.bundle().sendMessage(sender, "world.clone.failed", placeholder); - }); - - var image = plugin.imageProvider().load(plugin.imageProvider().createWorldImage() - .name(name).key(key).settings(settings).generator(generator).deletionType(deletion) - .environment(environment).worldType(type).autoSave(autoSave).generateStructures(structures) - .hardcore(hardcore).loadOnStart(!loadManual).seed(seed)); - - var message = image != null ? "world.create.success" : "world.create.failed"; - plugin.bundle().sendMessage(sender, message, Placeholder.parsed("world", name)); - if (image == null || !(sender instanceof Entity entity)) return; - entity.teleportAsync(image.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.COMMAND); + return world != null ? Command.SINGLE_SUCCESS : 0; } - private static boolean copy(File source, File destination) { - return source.isDirectory() ? copyDirectory(source, destination) : copyFile(source, destination); + private int createGenerator(CommandContext context, World.Environment environment, boolean structures, long seed) { + var generator = context.getArgument("generator", GeneratorArgument.Generator.class); + return create(context, environment, structures, seed, WorldType.NORMAL, null, generator); } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static boolean copyDirectory(File source, File destination) { - if (!destination.exists()) destination.mkdir(); - var list = source.list(); - if (list == null) return false; - List.of(list).forEach(file -> copy( - new File(source, file), - new File(destination, file) - )); - return true; + private int createPreset(CommandContext context, World.Environment environment, boolean structures, long seed) { + var preset = context.getArgument("preset", Preset.class); + return create(context, environment, structures, seed, WorldType.FLAT, preset, null); } - private static boolean copyFile(File source, File destination) { - if (source.getName().equals("uid.dat")) return true; - try (var in = new FileInputStream(source); - var out = new FileOutputStream(destination)) { - int length; - var buf = new byte[1024]; - while ((length = in.read(buf)) > 0) - out.write(buf, 0, length); - return true; - } catch (IOException ignored) { - return false; - } + private int createType(CommandContext context, World.Environment environment, boolean structures, long seed) { + var type = context.getArgument("type", WorldType.class); + return create(context, environment, structures, seed, type, null, null); + } + + private interface Creator { + int create(CommandContext context, World.Environment environment, boolean structures, long seed); } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldDeleteCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldDeleteCommand.java index 2ceb581b..c22c5dbd 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldDeleteCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldDeleteCommand.java @@ -1,48 +1,89 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.argument.CommandFlagsArgument; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; import org.bukkit.World; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.parser.flag.CommandFlag; + +import java.io.File; +import java.util.Set; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldDeleteCommand { - private final Worlds plugin; - private final Command.Builder builder; - - Command.Builder create() { - return builder.literal("delete") - .permission("worlds.command.world.delete") - .required("world", WorldParser.worldParser()) - .flag(CommandFlag.builder("keep-image")) - .flag(CommandFlag.builder("keep-world")) - .flag(CommandFlag.builder("schedule")) - .flag(CommandFlag.builder("confirm")) - .handler(this::execute); + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("delete") + .requires(source -> source.getSender().hasPermission("worlds.command.delete")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.argument("flags", new CommandFlagsArgument( + Set.of("--confirm", "--schedule") + )).executes(this::delete)) + .executes(this::confirmationNeeded)); + } + + private int confirmationNeeded(CommandContext context) { + var sender = context.getSource().getSender(); + plugin.bundle().sendMessage(sender, "command.confirmation", + Placeholder.parsed("action", "/" + context.getInput()), + Placeholder.parsed("confirmation", "/" + context.getInput() + " --confirm")); + return Command.SINGLE_SUCCESS; + } + + private int delete(CommandContext context) { + var flags = context.getArgument("flags", CommandFlagsArgument.Flags.class); + if (!flags.contains("--confirm")) return confirmationNeeded(context); + var world = context.getArgument("world", World.class); + var result = delete(world, flags.contains("--schedule")); + plugin.bundle().sendMessage(context.getSource().getSender(), result, + Placeholder.parsed("world", world.key().asString())); + return Command.SINGLE_SUCCESS; + } + + private String delete(World world, boolean schedule) { + + var dragonBattle = world.getEnderDragonBattle(); + if (dragonBattle != null) dragonBattle.getBossBar().removeAll(); + + return schedule ? scheduleDeletion(world) : deleteNow(world); + } + + private String deleteNow(World world) { + if (world.getKey().toString().equals("minecraft:overworld")) + return "world.delete.disallowed"; + + var fallback = plugin.getServer().getWorlds().getFirst().getSpawnLocation(); + world.getPlayers().forEach(player -> player.teleport(fallback)); + + if (!plugin.getServer().unloadWorld(world, false)) + return "world.unload.failed"; + + return delete(world.getWorldFolder()) ? "world.delete.success" : "world.delete.failed"; + } + + private String scheduleDeletion(World world) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (delete(world.getWorldFolder())) return; + plugin.getComponentLogger().error("Failed to delete world {}", world.getName()); + })); + return "world.delete.scheduled"; } - @SuppressWarnings("deprecation") - private void execute(CommandContext context) { - if (!context.flags().contains("confirm")) { - plugin.bundle().sendMessage(context.sender().getSender(), "command.confirmation", - Placeholder.parsed("action", "/" + context.rawInput().input()), - Placeholder.parsed("confirmation", "/" + context.rawInput().input() + " --confirm")); - return; - } - var world = context.get("world"); - var keepImage = context.flags().contains("keep-image"); - var keepWorld = context.flags().contains("keep-world"); - var schedule = context.flags().contains("schedule"); - var image = plugin.imageProvider().getOrDefault(world); - var result = image.delete(keepImage, keepWorld, schedule); - plugin.bundle().sendMessage(context.sender().getSender(), result.getMessage(), - Placeholder.parsed("world", world.getName()), - Placeholder.parsed("image", image.getWorldImage().name())); + private boolean delete(File file) { + if (file.isFile()) return file.delete(); + var files = file.listFiles(); + if (files == null) return false; + for (var file1 : files) delete(file1); + return file.delete(); } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldExportCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldExportCommand.java deleted file mode 100644 index f5e71d79..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldExportCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.thenextlvl.worlds.command; - -import io.papermc.paper.command.brigadier.CommandSourceStack; -import lombok.RequiredArgsConstructor; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.exception.InvalidSyntaxException; - -import java.util.List; - -@RequiredArgsConstructor -@SuppressWarnings("UnstableApiUsage") -class WorldExportCommand { - private final Worlds plugin; - private final Command.Builder builder; - - Command.Builder create() { - return builder.literal("export", "save") - .permission("worlds.command.world.export") - .optional("world", WorldParser.worldParser()) - .handler(this::execute); - } - - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var world = context.optional("world").orElse(sender instanceof Player self ? self.getWorld() : null); - if (world == null) throw new InvalidSyntaxException("world export [world]", context.sender(), List.of()); - var placeholder = Placeholder.parsed("world", world.getName()); - try { - world.save(); - plugin.bundle().sendMessage(sender, "world.save.success", placeholder); - } catch (Exception e) { - plugin.bundle().sendMessage(sender, "world.save.failed", placeholder); - plugin.getComponentLogger().error("Failed to save world {}", world.getName(), e); - } - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldImportCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldImportCommand.java index 226a1473..24d5f781 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldImportCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldImportCommand.java @@ -1,67 +1,77 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import net.thenextlvl.worlds.image.WorldImage; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.incendo.cloud.Command; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.parser.standard.StringParser; -import org.incendo.cloud.suggestion.Suggestion; -import org.incendo.cloud.suggestion.SuggestionProvider; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.argument.DimensionArgument; +import net.thenextlvl.worlds.command.suggestion.LevelSuggestionProvider; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Optional; + +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldImportCommand { - private final Worlds plugin; - private final Command.Builder builder; + private final WorldsPlugin plugin; - @SuppressWarnings("deprecation") - Command.Builder create() { - return builder.literal("import") - .permission("worlds.command.world.import") - .required("image", StringParser.greedyStringParser(), - SuggestionProvider.blocking((context, input) -> plugin.imageProvider().findImages().stream() - .map(WorldImage::name) - .filter(name -> Bukkit.getWorld(name) == null) - .map(Suggestion::suggestion) - .toList())) - .handler(this::execute); + ArgumentBuilder create() { + return Commands.literal("import") + .requires(source -> source.getSender().hasPermission("worlds.command.import")) + .then(Commands.argument("world", StringArgumentType.string()) + .suggests(new LevelSuggestionProvider<>(plugin)) + .then(Commands.argument("key", ArgumentTypes.namespacedKey()) + .executes(context -> { + var key = context.getArgument("key", NamespacedKey.class); + return execute(context, null, key); + })) + .then(Commands.argument("dimension", new DimensionArgument(plugin)) + .then(Commands.argument("key", ArgumentTypes.namespacedKey()) + .executes(context -> { + var environment = context.getArgument("dimension", World.Environment.class); + var key = context.getArgument("key", NamespacedKey.class); + return execute(context, environment, key); + })) + .executes(context -> { + var environment = context.getArgument("dimension", World.Environment.class); + return execute(context, environment, null); + })) + .executes(context -> execute(context, null, null))); } - private void execute(CommandContext context) { - var sender = context.sender().getSender(); + private int execute(CommandContext context, @Nullable World.Environment environment, @Nullable NamespacedKey key) { + var name = context.getArgument("world", String.class); + var level = new File(plugin.getServer().getWorldContainer(), name); - var imageName = context.get("image"); - var image = plugin.imageProvider().findImageFiles().stream() - .filter(file -> { - var worldImage = plugin.imageProvider().of(file); - return worldImage != null && worldImage.name().equals(imageName); - }) - .findFirst() - .map(plugin.imageProvider()::of) - .orElse(null); + var world = plugin.levelView().isLevel(level) ? environment != null + ? plugin.levelView().loadLevel(level, environment, key, Optional::isEmpty) + : plugin.levelView().loadLevel(level, key, Optional::isEmpty) : null; - if (image == null) { - plugin.bundle().sendMessage(sender, "image.exists.not", Placeholder.parsed("image", imageName)); - return; - } + var message = world != null ? "world.import.success" : "world.import.failed"; + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("world", world != null ? world.key().asString() + : key != null ? key.asString() : name)); + + if (world != null && context.getSource().getSender() instanceof Entity entity) + entity.teleportAsync(world.getSpawnLocation(), COMMAND); - var world = Bukkit.getWorld(image.name()); - var placeholder = Placeholder.parsed("world", world != null ? world.getName() : image.name()); if (world != null) { - plugin.bundle().sendMessage(sender, "world.known", placeholder); - return; + plugin.persistWorld(world, true); + plugin.levelView().saveLevelData(world, true); } - var result = plugin.imageProvider().load(image); - var message = result != null ? "world.import.success" : "world.import.failed"; - plugin.bundle().sendMessage(sender, message, placeholder); - if (result == null || !(sender instanceof Player player)) return; - player.teleportAsync(result.getWorld().getSpawnLocation(), PlayerTeleportEvent.TeleportCause.COMMAND); + return world != null ? Command.SINGLE_SUCCESS : 0; } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldInfoCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldInfoCommand.java index e2932a89..951d12ee 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldInfoCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldInfoCommand.java @@ -1,73 +1,84 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import core.nbt.tag.CompoundTag; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; import lombok.RequiredArgsConstructor; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.model.WorldPreset; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; import org.bukkit.World; import org.bukkit.WorldType; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.exception.InvalidSyntaxException; -import org.jetbrains.annotations.Nullable; -import java.util.List; +import java.util.Optional; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldInfoCommand { - private final Worlds plugin; - private final Command.Builder builder; + private final WorldsPlugin plugin; - Command.Builder create() { - return builder.literal("info") - .permission("worlds.command.world.info") - .optional("world", WorldParser.worldParser()) - .handler(this::execute); + ArgumentBuilder create() { + return Commands.literal("info") + .requires(source -> source.getSender().hasPermission("worlds.command.info")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .executes(context -> { + var world = context.getArgument("world", World.class); + return list(context.getSource().getSender(), world); + })) + .executes(context -> { + if (!(context.getSource().getSender() instanceof Player player)) { + plugin.bundle().sendMessage(context.getSource().getSender(), "command.sender"); + return 0; + } else return list(context.getSource().getSender(), player.getWorld()); + }); } @SuppressWarnings("deprecation") - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var world = context.optional("world").orElse(sender instanceof Player self ? self.getWorld() : null); - if (world == null) throw new InvalidSyntaxException("world info [world]", sender, List.of()); - var volume = plugin.imageProvider().getOrDefault(world); + private int list(CommandSender sender, World world) { + var root = plugin.levelView().getLevelDataFile(world.getWorldFolder()).getRoot(); + var data = root.optional("Data"); + var settings = data.flatMap(tag -> tag.optional("WorldGenSettings")); + var dimensions = settings.flatMap(tag -> tag.optional("dimensions")); + var dimension = dimensions.flatMap(tag -> tag.optional( + plugin.levelView().getDimension(tag, world.getEnvironment()))); + var generator = dimension.flatMap(tag -> tag.optional("generator")); + + var environment = dimensions.map(tag -> plugin.levelView().getDimension(tag, world.getEnvironment())); + var worldPreset = generator.flatMap(tag -> plugin.levelView().getWorldPreset(tag)); + plugin.bundle().sendMessage(sender, "world.info.name", - Placeholder.parsed("world", world.getName())); + Placeholder.parsed("world", world.key().asString()), + Placeholder.parsed("name", world.getName())); plugin.bundle().sendMessage(sender, "world.info.players", Placeholder.parsed("players", String.valueOf(world.getPlayers().size()))); plugin.bundle().sendMessage(sender, "world.info.type", - Placeholder.parsed("type", getName(notnull(world.getWorldType(), WorldType.NORMAL)))); - plugin.bundle().sendMessage(sender, "world.info.environment", - Placeholder.parsed("environment", getName(world.getEnvironment()))); - plugin.bundle().sendMessage(sender, "world.info.generator", - Placeholder.parsed("generator", String.valueOf(notnull( - volume.getWorldImage().generator(), "Vanilla")))); + Placeholder.parsed("type", worldPreset.map(WorldPreset::key) + .map(Key::asString).orElse("unknown")), + Placeholder.parsed("old", Optional.ofNullable(world.getWorldType()) + .orElse(WorldType.NORMAL).getName().toLowerCase())); + plugin.bundle().sendMessage(sender, "world.info.dimension", + Placeholder.parsed("dimension", environment.orElse("unknown"))); + getGenerator(world).ifPresent(gen -> plugin.bundle().sendMessage(sender, + "world.info.generator", Placeholder.parsed("generator", gen))); plugin.bundle().sendMessage(sender, "world.info.seed", Placeholder.parsed("seed", String.valueOf(world.getSeed()))); + return Command.SINGLE_SUCCESS; } - private static String getName(World.Environment environment) { - return switch (environment) { - case THE_END -> "The End"; - case NETHER -> "Nether"; - case NORMAL -> "Normal"; - case CUSTOM -> "Custom"; - }; - } - - private String getName(WorldType type) { - return switch (type) { - case LARGE_BIOMES -> "Large Biomes"; - case AMPLIFIED -> "Amplified"; - case NORMAL -> "Normal"; - case FLAT -> "Flat"; - }; - } - - private V notnull(@Nullable V value, V defaultValue) { - return value != null ? value : defaultValue; + private Optional getGenerator(World world) { + if (world.getGenerator() == null) return Optional.empty(); + var loader = world.getGenerator().getClass().getClassLoader(); + if (!(loader instanceof ConfiguredPluginClassLoader pluginLoader)) return Optional.empty(); + if (pluginLoader.getPlugin() == null) return Optional.empty(); + return Optional.of(pluginLoader.getPlugin().getName()); } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCommand.java index ba55c017..b775cfaf 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCommand.java @@ -1,154 +1,21 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.builder.ArgumentBuilder; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import lombok.RequiredArgsConstructor; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import net.thenextlvl.worlds.link.Link; -import org.bukkit.PortalType; -import org.bukkit.World; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.description.Description; -import org.incendo.cloud.exception.InvalidSyntaxException; -import org.incendo.cloud.parser.standard.EnumParser; -import org.incendo.cloud.parser.standard.StringParser; -import org.incendo.cloud.suggestion.Suggestion; -import org.incendo.cloud.suggestion.SuggestionProvider; +import net.thenextlvl.worlds.WorldsPlugin; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") -abstract class WorldLinkCommand { - protected final Worlds plugin; - protected final Command.Builder builder; - - protected final Command.Builder linkCommand() { - return builder.literal("link") - .commandDescription(Description.description("link portals between dimensions")); - } - - abstract Command.Builder create(); - - static class Create extends WorldLinkCommand { - public Create(Worlds plugin, Command.Builder builder) { - super(plugin, builder); - } - - @Override - Command.Builder create() { - return linkCommand().literal("create") - .permission("worlds.command.link.create") - .required("source", WorldParser.worldParser()) - .required("destination", WorldParser.worldParser()) - .optional("portal-type", EnumParser.enumParser(PortalType.class)) - .handler(this::execute); - } - - private void execute(CommandContext context) { - handleCreate(context); - } - - private void handleCreate(CommandContext context) { - var source = context.get("source"); - var destination = context.get("destination"); - var portalType = context.optional("portal-type") - .orElse(getPortalType(source.getEnvironment(), destination.getEnvironment())); - - var sender = context.sender().getSender(); - - if (portalType == null) throw new InvalidSyntaxException( - "world link create [source] [destination] [portal-type]", - sender, java.util.List.of() - ); - - var link = new Link(portalType, source, destination); - if (plugin.linkRegistry().register(link)) { - plugin.bundle().sendMessage(sender, "link.created", - Placeholder.parsed("type", link.portalType().name().toLowerCase()), - Placeholder.parsed("source", link.source().getName()), - Placeholder.parsed("destination", link.destination().getName())); - } else plugin.bundle().sendMessage(sender, "link.exists", - Placeholder.parsed("type", link.portalType().name().toLowerCase()), - Placeholder.parsed("source", link.source().getName()), - Placeholder.parsed("destination", link.destination().getName())); - } - - private PortalType getPortalType(World.Environment source, World.Environment destination) { - return switch (source) { - case NORMAL -> switch (destination) { - case NETHER -> PortalType.NETHER; - case THE_END -> PortalType.ENDER; - default -> null; - }; - case NETHER -> switch (destination) { - case THE_END -> PortalType.ENDER; - case NORMAL -> PortalType.NETHER; - default -> null; - }; - case THE_END -> switch (destination) { - case NORMAL -> PortalType.ENDER; - case NETHER -> PortalType.NETHER; - default -> null; - }; - default -> null; - }; - } - } - - static class Delete extends WorldLinkCommand { - public Delete(Worlds plugin, Command.Builder builder) { - super(plugin, builder); - } - - @Override - Command.Builder create() { - return linkCommand().literal("delete") - .permission("worlds.command.link.delete") - .required("link", StringParser.greedyStringParser(), - SuggestionProvider.blocking((context, input) -> plugin.linkRegistry().getLinks() - .map(Link::toString) - .map(Suggestion::suggestion) - .toList())) - .handler(this::execute); - } - - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var linkName = context.get("link"); - var link = plugin.linkRegistry().getLinks() - .filter(link1 -> link1.toString().equals(linkName)) - .findFirst() - .orElse(null); - if (link != null && plugin.linkRegistry().unregister(link)) { - plugin.bundle().sendMessage(sender, "link.deleted", - Placeholder.parsed("type", link.portalType().name().toLowerCase()), - Placeholder.parsed("source", link.source().getName()), - Placeholder.parsed("destination", link.destination().getName())); - } else plugin.bundle().sendMessage(sender, "link.exists.not", - Placeholder.parsed("link", linkName)); - } - } - - static class List extends WorldLinkCommand { - public List(Worlds plugin, Command.Builder builder) { - super(plugin, builder); - } - - @Override - Command.Builder create() { - return linkCommand().literal("list") - .permission("worlds.command.link.list") - .handler(this::execute); - } - - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var links = plugin.linkRegistry().getLinks().map(Link::toString).toList(); - if (links.isEmpty()) plugin.bundle().sendMessage(sender, "link.list.empty"); - else plugin.bundle().sendMessage(sender, "link.list", - Placeholder.parsed("links", String.join(", ", links)), - Placeholder.parsed("amount", String.valueOf(links.size()))); - } +class WorldLinkCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("link") + .requires(source -> source.getSender().hasPermission("worlds.command.link")) + .then(new WorldLinkCreateCommand(plugin).create()) + .then(new WorldLinkListCommand(plugin).create()) + .then(new WorldLinkRemoveCommand(plugin).create()); } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCreateCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCreateCommand.java new file mode 100644 index 00000000..7cbd2469 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkCreateCommand.java @@ -0,0 +1,39 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +public class WorldLinkCreateCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("create") + .requires(source -> source.getSender().hasPermission("worlds.command.link.create")) + .then(Commands.argument("source", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin, world -> + world.getEnvironment().equals(World.Environment.NORMAL))) + .then(Commands.argument("destination", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin, world -> + !world.getEnvironment().equals(World.Environment.NORMAL))) + .executes(context -> { + var source = context.getArgument("source", World.class); + var destination = context.getArgument("destination", World.class); + var link = plugin.linkController().link(source, destination); + var message = link ? "world.link.success" : "world.link.failed"; + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("source", source.key().asString()), + Placeholder.parsed("destination", destination.key().asString())); + return link ? Command.SINGLE_SUCCESS : 0; + }))); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkListCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkListCommand.java new file mode 100644 index 00000000..cda92517 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkListCommand.java @@ -0,0 +1,44 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.link.Relative; +import org.bukkit.World; + +import java.util.Arrays; +import java.util.Objects; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +public class WorldLinkListCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("list") + .requires(source -> source.getSender().hasPermission("worlds.command.link.list")) + .executes(this::list); + } + + private int list(CommandContext context) { + var sender = context.getSource().getSender(); + var links = plugin.getServer().getWorlds().stream() + .filter(world -> world.getEnvironment().equals(World.Environment.NORMAL)) + .mapMulti((world, consumer) -> Arrays.stream(Relative.values()) + .filter(relative -> !relative.equals(Relative.OVERWORLD)) + .map(relative -> plugin.linkController().getTarget(world, relative).orElse(null)) + .filter(Objects::nonNull) + .forEach(key -> consumer.accept(world.key().asString() + " <-> " + key.asString()))) + .toList(); + if (links.isEmpty()) plugin.bundle().sendMessage(sender, "world.link.list.empty"); + else plugin.bundle().sendMessage(sender, "world.link.list", + Placeholder.parsed("links", String.join(",", links)), + Placeholder.parsed("amount", String.valueOf(links.size()))); + return links.isEmpty() ? 0 : Command.SINGLE_SUCCESS; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkRemoveCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkRemoveCommand.java new file mode 100644 index 00000000..16e8253f --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLinkRemoveCommand.java @@ -0,0 +1,40 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.link.Relative; +import net.thenextlvl.worlds.command.argument.RelativeArgument; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +public class WorldLinkRemoveCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("remove") + .requires(source -> source.getSender().hasPermission("worlds.command.link.remove")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin, world -> + world.getEnvironment().equals(World.Environment.NORMAL))) + .then(Commands.argument("relative", new RelativeArgument(relative -> + !relative.equals(Relative.OVERWORLD))) + .executes(context -> { + var world = context.getArgument("world", World.class); + var relative = context.getArgument("relative", Relative.class); + var unlink = plugin.linkController().unlink(world, relative); + var message = unlink ? "world.unlink.success" : "world.unlink.failed"; + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("relative", relative.key().asString()), + Placeholder.parsed("world", world.key().asString())); + return unlink ? Command.SINGLE_SUCCESS : 0; + }))); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldListCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldListCommand.java index 471e0850..ca1bfd1d 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldListCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldListCommand.java @@ -1,33 +1,38 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import lombok.RequiredArgsConstructor; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.Bukkit; -import org.bukkit.generator.WorldInfo; -import org.incendo.cloud.Command; -import org.incendo.cloud.context.CommandContext; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.Keyed; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldListCommand { - private final Worlds plugin; - private final Command.Builder builder; + private final WorldsPlugin plugin; - Command.Builder create() { - return builder.literal("list") - .permission("worlds.command.world.list") - .handler(this::execute); + ArgumentBuilder create() { + return Commands.literal("list") + .requires(source -> source.getSender().hasPermission("worlds.command.list")) + .executes(this::list); } - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var worlds = Bukkit.getWorlds().stream().map(WorldInfo::getName).toList(); + private int list(CommandContext context) { + var sender = context.getSource().getSender(); + var worlds = plugin.getServer().getWorlds().stream() + .map(Keyed::key) + .map(Key::asString) + .toList(); + var joined = Component.join(JoinConfiguration.commas(true), worlds.stream() .map(world -> Component.text(world) .hoverEvent(HoverEvent.showText(plugin.bundle().component(sender, @@ -37,5 +42,7 @@ private void execute(CommandContext context) { plugin.bundle().sendMessage(sender, "world.list", Placeholder.parsed("amount", String.valueOf(worlds.size())), Placeholder.component("worlds", joined)); + + return Command.SINGLE_SUCCESS; } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLoadCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLoadCommand.java new file mode 100644 index 00000000..f54d53b6 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldLoadCommand.java @@ -0,0 +1,44 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.LevelSuggestionProvider; +import org.bukkit.entity.Entity; + +import java.io.File; + +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldLoadCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("load") + .requires(source -> source.getSender().hasPermission("worlds.command.load")) + .then(Commands.argument("world", StringArgumentType.string()) + .suggests(new LevelSuggestionProvider<>(plugin)) + .executes(this::load)); + } + + private int load(CommandContext context) { + var name = context.getArgument("world", String.class); + var level = new File(plugin.getServer().getWorldContainer(), name); + var world = plugin.levelView().isLevel(level) ? plugin.levelView().loadLevel(level, + optional -> optional.map(extras -> !extras.enabled()).isPresent()) : null; + var message = world != null ? "world.load.success" : "world.load.failed"; + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("world", world != null ? world.key().asString() : name)); + if (world != null && context.getSource().getSender() instanceof Entity entity) + entity.teleportAsync(world.getSpawnLocation(), COMMAND); + return world != null ? Command.SINGLE_SUCCESS : 0; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldRegenerateCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldRegenerateCommand.java new file mode 100644 index 00000000..c8cf99e2 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldRegenerateCommand.java @@ -0,0 +1,114 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.argument.CommandFlagsArgument; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; + +import java.io.File; +import java.util.Set; + +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldRegenerateCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("regenerate") + .requires(source -> source.getSender().hasPermission("worlds.command.regenerate")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.argument("flags", new CommandFlagsArgument( + Set.of("--confirm", "--schedule") + )).executes(this::regenerate)) + .executes(this::confirmationNeeded)); + } + + private int confirmationNeeded(CommandContext context) { + var sender = context.getSource().getSender(); + plugin.bundle().sendMessage(sender, "command.confirmation", + Placeholder.parsed("action", "/" + context.getInput()), + Placeholder.parsed("confirmation", "/" + context.getInput() + " --confirm")); + return Command.SINGLE_SUCCESS; + } + + private int regenerate(CommandContext context) { + var flags = context.getArgument("flags", CommandFlagsArgument.Flags.class); + if (!flags.contains("--confirm")) return confirmationNeeded(context); + var world = context.getArgument("world", World.class); + var result = regenerate(world, flags.contains("--schedule")); + plugin.bundle().sendMessage(context.getSource().getSender(), result, + Placeholder.parsed("world", world.key().asString())); + return Command.SINGLE_SUCCESS; + } + + private String regenerate(World world, boolean schedule) { + + var dragonBattle = world.getEnderDragonBattle(); + if (dragonBattle != null) dragonBattle.getBossBar().removeAll(); + + return schedule ? scheduleRegeneration(world) : regenerateNow(world); + } + + private String regenerateNow(World world) { + if (world.getKey().toString().equals("minecraft:overworld")) + return "world.regenerate.disallowed"; + + var environment = world.getEnvironment(); + var worldFolder = world.getWorldFolder(); + var players = world.getPlayers(); + + var fallback = plugin.getServer().getWorlds().getFirst().getSpawnLocation(); + players.forEach(player -> player.teleport(fallback, COMMAND)); + + plugin.persistWorld(world, true); + plugin.levelView().saveLevelData(world, false); + + if (!plugin.getServer().unloadWorld(world, false)) + return "world.unload.failed"; + + regenerate(worldFolder); + + var regenerated = plugin.levelView().loadLevel(worldFolder, environment); + if (regenerated != null) players.forEach(player -> + player.teleportAsync(regenerated.getSpawnLocation(), COMMAND)); + return regenerated != null ? "world.regenerate.success" : "world.regenerate.failed"; + } + + private String scheduleRegeneration(World world) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> + regenerate(world.getWorldFolder()))); + return "world.regenerate.scheduled"; + } + + private void regenerate(File level) { + delete(new File(level, "DIM-1")); + delete(new File(level, "DIM1")); + delete(new File(level, "advancements")); + delete(new File(level, "data")); + delete(new File(level, "entities")); + delete(new File(level, "playerdata")); + delete(new File(level, "poi")); + delete(new File(level, "region")); + delete(new File(level, "stats")); + } + + @SuppressWarnings("UnusedReturnValue") + private boolean delete(File file) { + if (file.isFile()) return file.delete(); + var files = file.listFiles(); + if (files == null) return false; + for (var file1 : files) delete(file1); + return file.delete(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveAllCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveAllCommand.java new file mode 100644 index 00000000..66c22972 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveAllCommand.java @@ -0,0 +1,32 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.craftbukkit.CraftServer; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldSaveAllCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("save-all") + .requires(source -> source.getSender().hasPermission("worlds.command.save-all")) + .then(Commands.literal("flush").executes(context -> saveAll(context.getSource(), true))) + .executes(context -> saveAll(context.getSource(), false)); + } + + private int saveAll(CommandSourceStack source, boolean flush) { + plugin.bundle().sendMessage(source.getSender(), "world.save.saving"); + var server = ((CraftServer) plugin.getServer()).getServer(); + var saved = server.saveEverything(!(source.getSender() instanceof ConsoleCommandSender), flush, true); + var message = saved ? "world.save.success" : "world.save.failed"; + plugin.bundle().sendMessage(source.getSender(), message); + return saved ? Command.SINGLE_SUCCESS : 0; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveCommand.java new file mode 100644 index 00000000..5cd4ad9c --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveCommand.java @@ -0,0 +1,38 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldSaveCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("save") + .requires(source -> source.getSender().hasPermission("worlds.command.save")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.literal("flush") + .executes(context -> save(context, true))) + .executes(context -> save(context, false))); + } + + private int save(CommandContext context, boolean flush) { + var world = context.getArgument("world", World.class); + var placeholder = Placeholder.parsed("world", world.key().asString()); + plugin.bundle().sendMessage(context.getSource().getSender(), "world.save", placeholder); + plugin.levelView().saveLevel(world, flush); + plugin.bundle().sendMessage(context.getSource().getSender(), "world.save.success", placeholder); + return Command.SINGLE_SUCCESS; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOffCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOffCommand.java new file mode 100644 index 00000000..03bf5781 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOffCommand.java @@ -0,0 +1,36 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; +import org.bukkit.command.CommandSender; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldSaveOffCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("save-off") + .requires(source -> source.getSender().hasPermission("worlds.command.save-off")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .executes(context -> saveOff(context.getSource().getSender(), + context.getArgument("world", World.class)))) + .executes(context -> saveOff(context.getSource().getSender(), + context.getSource().getLocation().getWorld())); + } + + private int saveOff(CommandSender sender, World world) { + var message = world.isAutoSave() ? "world.save.off" : "world.save.already-off"; + world.setAutoSave(false); + plugin.bundle().sendMessage(sender, message); + return Command.SINGLE_SUCCESS; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOnCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOnCommand.java new file mode 100644 index 00000000..fd2d13f1 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSaveOnCommand.java @@ -0,0 +1,36 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; +import org.bukkit.command.CommandSender; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldSaveOnCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("save-on") + .requires(source -> source.getSender().hasPermission("worlds.command.save-on")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .executes(context -> saveOn(context.getSource().getSender(), + context.getArgument("world", World.class)))) + .executes(context -> saveOn(context.getSource().getSender(), + context.getSource().getLocation().getWorld())); + } + + private int saveOn(CommandSender sender, World world) { + var message = world.isAutoSave() ? "world.save.already-on" : "world.save.on"; + world.setAutoSave(true); + plugin.bundle().sendMessage(sender, message); + return Command.SINGLE_SUCCESS; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSetSpawnCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSetSpawnCommand.java index 8fc5da88..5c073315 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSetSpawnCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSetSpawnCommand.java @@ -1,62 +1,64 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.Location; -import org.bukkit.entity.Player; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.location.LocationParser; -import org.incendo.cloud.component.DefaultValue; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.exception.InvalidCommandSenderException; -import org.incendo.cloud.parser.standard.FloatParser; - -import java.util.List; - -import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.World; +import org.bukkit.command.CommandSender; @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldSetSpawnCommand { - private final Worlds plugin; - private final Command.Builder builder; + private final WorldsPlugin plugin; - Command.Builder create() { - return builder.literal("setspawn") - .permission("worlds.command.world.setspawn") - // .senderType(Player.class) - .optional("position", LocationParser.locationParser(), - DefaultValue.dynamic(context -> context.sender().getLocation())) - .optional("angle", FloatParser.floatParser(-360, 360), - DefaultValue.dynamic(context -> { - var executor = context.sender().getExecutor(); - return executor != null ? executor.getYaw() : 0; + ArgumentBuilder create() { + return Commands.literal("setspawn") + .requires(source -> source.getSender().hasPermission("worlds.command.setspawn")) + .then(Commands.argument("position", ArgumentTypes.blockPosition()) + .then(Commands.argument("angle", FloatArgumentType.floatArg(-180, 180)) + .executes(context -> { + var angle = context.getArgument("angle", float.class); + var resolver = context.getArgument("position", BlockPositionResolver.class); + var position = resolver.resolve(context.getSource()); + return setSpawn(context.getSource().getSender(), + context.getSource().getLocation().getWorld(), + position.blockX(), position.blockY(), position.blockZ(), angle + ); + })) + .executes(context -> { + var resolver = context.getArgument("position", BlockPositionResolver.class); + var position = resolver.resolve(context.getSource()); + return setSpawn(context.getSource().getSender(), + context.getSource().getLocation().getWorld(), + position.blockX(), position.blockY(), position.blockZ(), 0 + ); })) - .handler(this::execute); + .executes(context -> { + var location = context.getSource().getLocation(); + return setSpawn(context.getSource().getSender(), + location.getWorld(), + location.blockX(), + location.blockY(), + location.blockZ(), 0 + ); + }); } - private void execute(CommandContext context) { - if (!(context.sender().getSender() instanceof Player player)) - throw new InvalidCommandSenderException(context.sender(), Player.class, List.of(), context.command()); - - var location = context.get("position"); - float angle = context.get("angle"); - - var success = player.getWorld().setSpawnLocation( - location.getBlockX(), - location.getBlockY(), - location.getBlockZ(), - angle - ); - if (success) player.teleportAsync(player.getWorld().getSpawnLocation(), COMMAND); - + private int setSpawn(CommandSender sender, World world, int x, int y, int z, float angle) { + var success = world.setSpawnLocation(x, y, z, angle); var message = success ? "world.spawn.set.success" : "world.spawn.set.failed"; - plugin.bundle().sendMessage(player, message, - Placeholder.parsed("x", String.valueOf(location.getBlockX())), - Placeholder.parsed("y", String.valueOf(location.getBlockY())), - Placeholder.parsed("z", String.valueOf(location.getBlockZ())), + plugin.bundle().sendMessage(sender, message, + Placeholder.parsed("x", String.valueOf(x)), + Placeholder.parsed("y", String.valueOf(y)), + Placeholder.parsed("z", String.valueOf(z)), Placeholder.parsed("angle", String.valueOf(angle))); + return success ? Command.SINGLE_SUCCESS : 0; } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSpawnCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSpawnCommand.java new file mode 100644 index 00000000..01be84ab --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldSpawnCommand.java @@ -0,0 +1,31 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.entity.Player; + +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldSpawnCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("spawn") + .requires(source -> source.getSender().hasPermission("worlds.command.spawn") + && source.getSender() instanceof Player) + .executes(context -> { + var player = (Player) context.getSource().getSender(); + player.teleportAsync(player.getWorld().getSpawnLocation(), COMMAND); + plugin.bundle().sendMessage(player, "world.teleport.self", + Placeholder.parsed("world", player.getWorld().key().asString())); + return Command.SINGLE_SUCCESS; + }); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldTeleportCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldTeleportCommand.java index ebc3fb71..98634333 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldTeleportCommand.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldTeleportCommand.java @@ -1,42 +1,84 @@ package net.thenextlvl.worlds.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver; import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.thenextlvl.worlds.Worlds; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.Location; import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerTeleportEvent; -import org.incendo.cloud.Command; -import org.incendo.cloud.bukkit.parser.PlayerParser; -import org.incendo.cloud.bukkit.parser.WorldParser; -import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.exception.InvalidSyntaxException; import java.util.List; +import static org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND; + @RequiredArgsConstructor @SuppressWarnings("UnstableApiUsage") class WorldTeleportCommand { - private final Worlds plugin; - private final Command.Builder builder; - - Command.Builder create() { - return builder.literal("teleport", "tp") - .permission("worlds.command.world.teleport") - .required("world", WorldParser.worldParser()) - .optional("player", PlayerParser.playerParser()) - .handler(this::execute); + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("teleport") + .requires(source -> source.getSender().hasPermission("worlds.command.teleport")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.argument("entities", ArgumentTypes.entities()) + .then(Commands.argument("position", ArgumentTypes.finePosition(true)) + .executes(this::teleportEntitiesPosition)) + .executes(this::teleportEntities)) + .executes(this::teleport)); + } + + private int teleportEntitiesPosition(CommandContext context) throws CommandSyntaxException { + var entities = context.getArgument("entities", EntitySelectorArgumentResolver.class); + var position = context.getArgument("position", FinePositionResolver.class); + var world = context.getArgument("world", World.class); + var location = position.resolve(context.getSource()).toLocation(world); + var resolved = entities.resolve(context.getSource()); + return teleport(context.getSource().getSender(), resolved, location); + } + + private int teleportEntities(CommandContext context) throws CommandSyntaxException { + var entities = context.getArgument("entities", EntitySelectorArgumentResolver.class); + var world = context.getArgument("world", World.class); + var resolved = entities.resolve(context.getSource()); + return teleport(context.getSource().getSender(), resolved, world.getSpawnLocation()); + } + + private int teleport(CommandContext context) { + if (!(context.getSource().getSender() instanceof Player player)) { + plugin.bundle().sendMessage(context.getSource().getSender(), "command.sender"); + return 0; + } + var world = context.getArgument("world", World.class); + return teleport(player, List.of(player), world.getSpawnLocation()); } - private void execute(CommandContext context) { - var sender = context.sender().getSender(); - var world = context.get("world"); - var player = context.optional("player").orElse(sender instanceof Player self ? self : null); - if (player == null) throw new InvalidSyntaxException("world teleport [world] [player]", sender, List.of()); - player.teleportAsync(world.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.COMMAND); - var message = player.equals(sender) ? "world.teleport.player.self" : "world.teleport.player.other"; - plugin.bundle().sendMessage(sender, message, Placeholder.parsed("world", world.getName()), - Placeholder.parsed("player", player.getName())); + private int teleport(CommandSender sender, List entities, Location location) { + var message = entities.size() == 1 ? "world.teleport.other" + : entities.isEmpty() ? "world.teleport.none" : "world.teleport.others"; + entities.forEach(entity -> { + entity.teleportAsync(location, COMMAND); + plugin.bundle().sendMessage(entity, "world.teleport.self", + Placeholder.parsed("world", location.getWorld().key().asString())); + }); + if (entities.size() == 1 && entities.getFirst().equals(sender)) return Command.SINGLE_SUCCESS; + plugin.bundle().sendMessage(sender, message, + Placeholder.component("entity", entities.isEmpty() ? Component.empty() : entities.getFirst().name()), + Placeholder.parsed("world", location.getWorld().key().asString()), + Placeholder.parsed("entities", String.valueOf(entities.size()))); + return entities.isEmpty() ? 0 : Command.SINGLE_SUCCESS; } } diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/WorldUnloadCommand.java b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldUnloadCommand.java new file mode 100644 index 00000000..1cf16c4e --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/WorldUnloadCommand.java @@ -0,0 +1,64 @@ +package net.thenextlvl.worlds.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldSuggestionProvider; +import org.bukkit.World; +import org.jetbrains.annotations.Nullable; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +class WorldUnloadCommand { + private final WorldsPlugin plugin; + + ArgumentBuilder create() { + return Commands.literal("unload") + .requires(source -> source.getSender().hasPermission("worlds.command.unload")) + .then(Commands.argument("world", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .then(Commands.argument("fallback", ArgumentTypes.world()) + .suggests(new WorldSuggestionProvider<>(plugin)) + .executes(context -> { + var world = context.getArgument("world", World.class); + var fallback = context.getArgument("fallback", World.class); + var message = unload(world, fallback); + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("world", world.key().asString())); + return Command.SINGLE_SUCCESS; + })) + .executes(context -> { + var world = context.getArgument("world", World.class); + var message = unload(world, null); + plugin.bundle().sendMessage(context.getSource().getSender(), message, + Placeholder.parsed("world", world.key().asString())); + return Command.SINGLE_SUCCESS; + })); + } + + private String unload(World world, @Nullable World fallback) { + if (world.equals(fallback)) return "world.unload.fallback"; + if (world.getKey().toString().equals("minecraft:overworld")) + return "world.unload.disallowed"; + + var fallbackSpawn = fallback != null ? fallback.getSpawnLocation() + : plugin.getServer().getWorlds().getFirst().getSpawnLocation(); + world.getPlayers().forEach(player -> player.teleport(fallbackSpawn)); + + plugin.persistWorld(world, false); + + var dragonBattle = world.getEnderDragonBattle(); + if (dragonBattle != null) dragonBattle.getBossBar().removeAll(); + + if (!world.isAutoSave()) plugin.levelView().saveLevelData(world, false); + + return plugin.getServer().unloadWorld(world, world.isAutoSave()) + ? "world.unload.success" + : "world.unload.failed"; + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/CommandFlagsArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/CommandFlagsArgument.java new file mode 100644 index 00000000..09629333 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/CommandFlagsArgument.java @@ -0,0 +1,34 @@ +package net.thenextlvl.worlds.command.argument; + +import com.mojang.brigadier.arguments.StringArgumentType; +import core.paper.command.WrappedArgumentType; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class CommandFlagsArgument extends WrappedArgumentType { + public CommandFlagsArgument(Set flags) { + super(StringArgumentType.greedyString(), (reader, type) -> { + var split = type.split(" "); + if (Arrays.stream(split).anyMatch(s -> !flags.contains(s))) + throw new IllegalArgumentException("unrecognized flag"); + return new Flags(split); + }, (context, builder) -> { + var index = builder.getRemaining().lastIndexOf(' ') + 1; + var substring = builder.getRemaining().substring(index); + flags.stream() + .filter(flag -> !builder.getRemaining().contains(flag)) + .filter(flag -> flag.startsWith(substring)) + .forEach(s -> builder.suggest(builder.getRemaining() + s.substring(substring.length()))); + return builder.buildFuture(); + }); + } + + public static class Flags extends HashSet { + private Flags(@NotNull String... flags) { + super(Set.of(flags)); + } + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/DimensionArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/DimensionArgument.java new file mode 100644 index 00000000..34e5ec14 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/DimensionArgument.java @@ -0,0 +1,20 @@ +package net.thenextlvl.worlds.command.argument; + +import core.paper.command.WrappedArgumentType; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import net.kyori.adventure.key.Key; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.DimensionSuggestionProvider; +import org.bukkit.World; + +@SuppressWarnings("UnstableApiUsage") +public class DimensionArgument extends WrappedArgumentType { + public DimensionArgument(WorldsPlugin plugin) { + super(ArgumentTypes.key(), (reader, type) -> switch (type.asString()) { + case "minecraft:overworld" -> World.Environment.NORMAL; + case "minecraft:the_end" -> World.Environment.THE_END; + case "minecraft:the_nether" -> World.Environment.NETHER; + default -> throw new IllegalArgumentException("Custom dimensions are not yet supported"); + }, new DimensionSuggestionProvider(plugin)); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/GeneratorArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/GeneratorArgument.java new file mode 100644 index 00000000..923fe26e --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/GeneratorArgument.java @@ -0,0 +1,25 @@ +package net.thenextlvl.worlds.command.argument; + +import com.google.common.base.Preconditions; +import com.mojang.brigadier.arguments.StringArgumentType; +import core.paper.command.WrappedArgumentType; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.GeneratorSuggestionProvider; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; + +public class GeneratorArgument extends WrappedArgumentType { + public GeneratorArgument(WorldsPlugin plugin) { + super(StringArgumentType.string(), (reader, type) -> { + var split = type.split(":", 2); + var generator = plugin.getServer().getPluginManager().getPlugin(split[0]); + Preconditions.checkNotNull(generator, "Unknown plugin"); + Preconditions.checkState(generator.isEnabled(), "Plugin is not enabled"); + Preconditions.checkState(plugin.generatorView().hasGenerator(generator), "Plugin has no generator"); + return new Generator(generator, split.length > 1 ? split[1] : null); + }, new GeneratorSuggestionProvider(plugin)); + } + + public record Generator(Plugin plugin, @Nullable String id) { + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/RelativeArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/RelativeArgument.java new file mode 100644 index 00000000..b31c9d86 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/RelativeArgument.java @@ -0,0 +1,18 @@ +package net.thenextlvl.worlds.command.argument; + +import core.paper.command.WrappedArgumentType; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import net.kyori.adventure.key.Key; +import net.thenextlvl.worlds.api.link.Relative; +import net.thenextlvl.worlds.command.suggestion.RelativeSuggestionProvider; + +import java.util.function.Predicate; + +@SuppressWarnings("UnstableApiUsage") +public class RelativeArgument extends WrappedArgumentType { + public RelativeArgument(Predicate filter) { + super(ArgumentTypes.key(), (reader, type) -> Relative.valueOf(type).orElseThrow(() -> + new IllegalArgumentException("Unknown relative: " + type.asString())), + new RelativeSuggestionProvider(filter)); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/SeedArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/SeedArgument.java new file mode 100644 index 00000000..069f5deb --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/SeedArgument.java @@ -0,0 +1,16 @@ +package net.thenextlvl.worlds.command.argument; + +import com.mojang.brigadier.arguments.StringArgumentType; +import core.paper.command.WrappedArgumentType; + +public class SeedArgument extends WrappedArgumentType { + public SeedArgument() { + super(StringArgumentType.string(), (reader, type) -> { + try { + return Long.parseLong(type); + } catch (NumberFormatException ignored) { + return (long) type.hashCode(); + } + }); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldPresetArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldPresetArgument.java new file mode 100644 index 00000000..bff402cb --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldPresetArgument.java @@ -0,0 +1,24 @@ +package net.thenextlvl.worlds.command.argument; + +import com.google.gson.JsonObject; +import com.mojang.brigadier.arguments.StringArgumentType; +import core.file.format.JsonFile; +import core.io.IO; +import core.paper.command.WrappedArgumentType; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.preset.Preset; +import net.thenextlvl.worlds.command.suggestion.WorldPresetSuggestionProvider; + +import java.io.File; + +public class WorldPresetArgument extends WrappedArgumentType { + public WorldPresetArgument(WorldsPlugin plugin) { + super(StringArgumentType.string(), (reader, type) -> { + var file = new File(plugin.presetsFolder(), type + ".json"); + if (!file.exists()) throw new IllegalStateException("No preset found"); + var root = new JsonFile<>(IO.of(file), new JsonObject()).getRoot(); + if (root.isJsonObject()) return Preset.deserialize(root); + throw new IllegalStateException("Not a valid preset"); + }, new WorldPresetSuggestionProvider(plugin)); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldTypeArgument.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldTypeArgument.java new file mode 100644 index 00000000..a6fceff5 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/WorldTypeArgument.java @@ -0,0 +1,23 @@ +package net.thenextlvl.worlds.command.argument; + +import core.paper.command.WrappedArgumentType; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import net.kyori.adventure.key.Key; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.command.suggestion.WorldTypeSuggestionProvider; +import org.bukkit.WorldType; + +@SuppressWarnings("UnstableApiUsage") +public class WorldTypeArgument extends WrappedArgumentType { + public WorldTypeArgument(WorldsPlugin plugin) { + super(ArgumentTypes.key(), (reader, type) -> switch (type.asString()) { + case "minecraft:amplified" -> WorldType.AMPLIFIED; + case "minecraft:flat" -> WorldType.FLAT; + case "minecraft:large_biomes" -> WorldType.LARGE_BIOMES; + case "minecraft:normal" -> WorldType.NORMAL; + // case "minecraft:single_biome" -> ; + // case "minecraft:debug_world" -> ; + default -> throw new IllegalArgumentException("Custom dimensions are not yet supported"); + }, new WorldTypeSuggestionProvider(plugin)); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/argument/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/package-info.java new file mode 100644 index 00000000..2902104b --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/argument/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@MethodsReturnNotNullByDefault +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.command.argument; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/api/src/main/java/net/thenextlvl/worlds/preset/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/command/package-info.java similarity index 89% rename from api/src/main/java/net/thenextlvl/worlds/preset/package-info.java rename to plugin/src/main/java/net/thenextlvl/worlds/command/package-info.java index 34de6e5d..c1080462 100644 --- a/api/src/main/java/net/thenextlvl/worlds/preset/package-info.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/package-info.java @@ -1,8 +1,8 @@ @TypesAreNotNullByDefault @FieldsAreNotNullByDefault -@ParametersAreNotNullByDefault @MethodsReturnNotNullByDefault -package net.thenextlvl.worlds.preset; +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.command; import core.annotation.FieldsAreNotNullByDefault; import core.annotation.MethodsReturnNotNullByDefault; diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/DimensionSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/DimensionSuggestionProvider.java new file mode 100644 index 00000000..05485924 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/DimensionSuggestionProvider.java @@ -0,0 +1,34 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import core.paper.command.SuggestionProvider; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +public class DimensionSuggestionProvider implements SuggestionProvider { + private final WorldsPlugin plugin; + + private final Map dimensions = Map.of( + "minecraft:overworld", "environment.normal", + "minecraft:the_end", "environment.end", + "minecraft:the_nether", "environment.nether" + ); + + @Override + public CompletableFuture suggest(CommandContext context, SuggestionsBuilder builder) { + var sender = ((CommandSourceStack) context.getSource()).getSender(); + dimensions.entrySet().stream() + .filter(entry -> entry.getKey().contains(builder.getRemaining())) + .forEach(entry -> builder.suggest(entry.getKey(), () -> + plugin.bundle().format(sender, entry.getValue()))); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/GeneratorSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/GeneratorSuggestionProvider.java new file mode 100644 index 00000000..aebc855b --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/GeneratorSuggestionProvider.java @@ -0,0 +1,28 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import core.paper.command.SuggestionProvider; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.plugin.Plugin; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +public class GeneratorSuggestionProvider implements SuggestionProvider { + private final WorldsPlugin plugin; + + @Override + public CompletableFuture suggest(CommandContext context, SuggestionsBuilder builder) { + Arrays.stream(plugin.getServer().getPluginManager().getPlugins()) + .filter(Plugin::isEnabled) + .filter(plugin.generatorView()::hasGenerator) + .map(Plugin::getName) + .filter(s -> s.contains(builder.getRemaining())) + .forEach(builder::suggest); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/LevelSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/LevelSuggestionProvider.java new file mode 100644 index 00000000..26f6d404 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/LevelSuggestionProvider.java @@ -0,0 +1,28 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; + +import java.io.File; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +public class LevelSuggestionProvider implements SuggestionProvider { + private final WorldsPlugin plugin; + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { + plugin.levelView().listLevels() + .filter(plugin.levelView()::canLoad) + .map(File::getName) + .map(StringArgumentType::escapeIfRequired) + .filter(s -> s.contains(builder.getRemaining())) + .forEach(builder::suggest); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/RelativeSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/RelativeSuggestionProvider.java new file mode 100644 index 00000000..0c39a5e2 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/RelativeSuggestionProvider.java @@ -0,0 +1,27 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import core.paper.command.SuggestionProvider; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.api.link.Relative; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +@RequiredArgsConstructor +public class RelativeSuggestionProvider implements SuggestionProvider { + private final Predicate filter; + + @Override + public CompletableFuture suggest(CommandContext context, SuggestionsBuilder builder) { + Arrays.stream(Relative.values()) + .filter(filter) + .map(relative -> relative.key().asString()) + .filter(s -> s.contains(builder.getRemaining())) + .forEach(builder::suggest); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldPresetSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldPresetSuggestionProvider.java new file mode 100644 index 00000000..387c7c0a --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldPresetSuggestionProvider.java @@ -0,0 +1,31 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import core.paper.command.SuggestionProvider; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; + +import java.io.File; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +public class WorldPresetSuggestionProvider implements SuggestionProvider { + private final WorldsPlugin plugin; + + @Override + public CompletableFuture suggest(CommandContext context, SuggestionsBuilder builder) { + var files = plugin.presetsFolder().listFiles((file, name) -> + name.endsWith(".json")); + if (files != null) Arrays.stream(files) + .map(File::getName) + .map(name -> name.substring(0, name.length() - 5)) + .map(StringArgumentType::escapeIfRequired) + .filter(s -> s.contains(builder.getRemaining())) + .forEach(builder::suggest); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldSuggestionProvider.java new file mode 100644 index 00000000..5f08f9ee --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldSuggestionProvider.java @@ -0,0 +1,33 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.key.Key; +import org.bukkit.Keyed; +import org.bukkit.World; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +@AllArgsConstructor +@RequiredArgsConstructor +public class WorldSuggestionProvider implements SuggestionProvider { + private final Plugin plugin; + private Predicate filter = world -> true; + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { + plugin.getServer().getWorlds().stream() + .filter(filter) + .map(Keyed::key) + .map(Key::asString) + .filter(s -> s.contains(builder.getRemaining())) + .forEach(builder::suggest); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldTypeSuggestionProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldTypeSuggestionProvider.java new file mode 100644 index 00000000..dcbc6fc6 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/WorldTypeSuggestionProvider.java @@ -0,0 +1,35 @@ +package net.thenextlvl.worlds.command.suggestion; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import core.paper.command.SuggestionProvider; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@RequiredArgsConstructor +@SuppressWarnings("UnstableApiUsage") +public class WorldTypeSuggestionProvider implements SuggestionProvider { + private final WorldsPlugin plugin; + + private final Map dimensions = Map.of( + "minecraft:amplified", "world.type.amplified", + "minecraft:flat", "world.type.flat", + "minecraft:large_biomes", "world.type.large_biomes", + "minecraft:normal", "world.type.normal" + ); + + @Override + public CompletableFuture suggest(CommandContext context, SuggestionsBuilder builder) { + var sender = ((CommandSourceStack) context.getSource()).getSender(); + dimensions.entrySet().stream() + .filter(entry -> entry.getKey().contains(builder.getRemaining())) + .forEach(entry -> builder.suggest(entry.getKey(), () -> + plugin.bundle().format(sender, entry.getValue()))); + return builder.buildFuture(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/package-info.java new file mode 100644 index 00000000..88600988 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/command/suggestion/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@MethodsReturnNotNullByDefault +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.command.suggestion; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/plugin/src/main/java/net/thenextlvl/worlds/controller/WorldLinkController.java b/plugin/src/main/java/net/thenextlvl/worlds/controller/WorldLinkController.java new file mode 100644 index 00000000..940ec100 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/controller/WorldLinkController.java @@ -0,0 +1,89 @@ +package net.thenextlvl.worlds.controller; + +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.link.LinkController; +import net.thenextlvl.worlds.api.link.Relative; +import org.bukkit.NamespacedKey; +import org.bukkit.PortalType; +import org.bukkit.World; + +import java.util.Optional; + +import static org.bukkit.persistence.PersistentDataType.STRING; + +@RequiredArgsConstructor +public class WorldLinkController implements LinkController { + private final WorldsPlugin plugin; + + @Override + public Optional getTarget(World world, Relative relative) { + return Optional.ofNullable(world.getPersistentDataContainer() + .get(relative.key(), STRING) + ).map(NamespacedKey::fromString); + } + + @Override + public Optional getTarget(World world, PortalType type) { + return switch (type) { + case NETHER -> switch (world.getEnvironment()) { + case NORMAL, THE_END -> getTarget(world, Relative.NETHER); + case NETHER -> getTarget(world, Relative.OVERWORLD); + default -> Optional.empty(); + }; + case ENDER -> switch (world.getEnvironment()) { + case NORMAL, NETHER -> getTarget(world, Relative.THE_END); + case THE_END -> getTarget(world, Relative.OVERWORLD); + default -> Optional.empty(); + }; + default -> Optional.empty(); + }; + } + + @Override + public Optional getTarget(World world, World.Environment type) { + return switch (type) { + case NETHER -> getTarget(world, Relative.NETHER); + case THE_END -> getTarget(world, Relative.THE_END); + case NORMAL -> getTarget(world, Relative.OVERWORLD); + default -> Optional.empty(); + }; + } + + @Override + public boolean canLink(World source, World destination) { + return source.getEnvironment().equals(World.Environment.NORMAL) + && !destination.getEnvironment().equals(World.Environment.NORMAL) + && getTarget(source, destination.getEnvironment()).isEmpty(); + } + + @Override + public boolean link(World source, World destination) { + if (!canLink(source, destination)) return false; + var child = switch (destination.getEnvironment()) { + case NETHER -> Relative.NETHER; + case THE_END -> Relative.THE_END; + default -> null; + }; + if (child == null) return false; + var opposite = child.equals(Relative.NETHER) ? Relative.THE_END : Relative.NETHER; + getTarget(source, opposite).map(plugin.getServer()::getWorld).ifPresent(sibling -> { + sibling.getPersistentDataContainer().set(child.key(), STRING, destination.key().asString()); + destination.getPersistentDataContainer().set(opposite.key(), STRING, sibling.key().asString()); + }); + destination.getPersistentDataContainer().set(Relative.OVERWORLD.key(), STRING, source.key().asString()); + source.getPersistentDataContainer().set(child.key(), STRING, destination.key().asString()); + return true; + } + + @Override + public boolean unlink(World source, Relative relative) { + var world = getTarget(source, relative).map(plugin.getServer()::getWorld); + var parent = Relative.valueOf(source.getEnvironment()).map(Relative::key); + parent.ifPresent(key -> world.ifPresent(destination -> { + destination.getPersistentDataContainer().remove(key); + source.getPersistentDataContainer().remove(relative.key()); + })); + return world.isPresent(); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImage.java b/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImage.java deleted file mode 100644 index 6d9410b3..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImage.java +++ /dev/null @@ -1,113 +0,0 @@ -package net.thenextlvl.worlds.image; - -import core.file.FileIO; -import core.io.IO; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.Bukkit; -import org.bukkit.World; - -import java.io.File; -import java.io.IOException; - -@Getter -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -class CraftImage implements Image { - private final FileIO file; - private final Worlds plugin; - private final World world; - - CraftImage(Worlds plugin, World world, WorldImage image) { - this(CraftImageProvider.loadFile(IO.of(Bukkit.getWorldContainer(), image.name() + ".image"), image), plugin, world); - } - - CraftImage(Worlds plugin, World world) { - this(plugin, world, plugin.imageProvider().of(world)); - } - - @Override - public CraftImage save() { - file.save(); - return this; - } - - @Override - public WorldImage getWorldImage() { - return getFile().getRoot(); - } - - @Override - public boolean canUnload() { - return !Bukkit.isTickingWorlds() && world.getPlayers().isEmpty(); - } - - @Override - public boolean canDelete() { - return getWorld().getKey().toString().equals("minecraft:overworld"); - } - - @Override - public boolean unload() { - return canUnload() && Bukkit.unloadWorld(world, world.isAutoSave()); - } - - @Override - public DeleteResult delete(boolean keepImage, boolean keepWorld, boolean schedule) { - return schedule ? scheduleDeletion(keepImage, keepWorld) : deleteNow(keepImage, keepWorld); - } - - @Override - public DeleteResult deleteNow(boolean keepImage, boolean keepWorld) { - if (canDelete()) return DeleteResult.WORLD_DELETE_ILLEGAL; - - var fallback = Bukkit.getWorlds().getFirst().getSpawnLocation(); - getWorld().getPlayers().forEach(player -> player.teleport(fallback)); - - try { - if (!keepImage && file.getIO().exists() && !file.delete()) - return DeleteResult.IMAGE_DELETE_FAILED; - } catch (IOException e) { - return DeleteResult.IMAGE_DELETE_FAILED; - } - - if (keepImage && keepWorld) - return Bukkit.unloadWorld(world, world.isAutoSave()) - ? DeleteResult.WORLD_UNLOADED - : DeleteResult.WORLD_UNLOAD_FAILED; - - if (!Bukkit.unloadWorld(world, world.isAutoSave() && keepWorld)) - return DeleteResult.WORLD_UNLOAD_FAILED; - - if (!keepWorld && !delete(world.getWorldFolder())) - return DeleteResult.WORLD_DELETE_FAILED; - - return DeleteResult.WORLD_DELETED; - } - - @Override - public DeleteResult scheduleDeletion(boolean keepImage, boolean keepWorld) { - if (keepWorld && (keepImage || !getFile().getIO().exists())) - return DeleteResult.WORLD_DELETE_NOTHING; - - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - if (!keepWorld) delete(getWorld().getWorldFolder()); - if (!keepImage) try { - getFile().delete(); - } catch (IOException e) { - plugin.getComponentLogger().error("Failed to delete world {}", getWorld().getName(), e); - } - })); - - return DeleteResult.WORLD_DELETE_SCHEDULED; - } - - private boolean delete(File file) { - if (file.isFile()) return file.delete(); - var files = file.listFiles(); - if (files == null) return false; - for (var file1 : files) delete(file1); - return file.delete(); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImageProvider.java b/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImageProvider.java deleted file mode 100644 index a71fff9f..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftImageProvider.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.thenextlvl.worlds.image; - -import com.google.gson.GsonBuilder; -import core.file.FileIO; -import core.file.format.GsonFile; -import core.io.IO; -import core.paper.adapters.key.KeyAdapter; -import lombok.RequiredArgsConstructor; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.Bukkit; -import org.bukkit.NamespacedKey; -import org.bukkit.World; -import org.bukkit.WorldType; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.util.*; - -@RequiredArgsConstructor -public class CraftImageProvider implements ImageProvider { - private static final Map images = new HashMap<>(); - private final Worlds plugin; - - @Override - public @Nullable Image load(@Nullable WorldImage image) { - if (image == null || Bukkit.getWorld(image.name()) != null) return null; - var build = image.build(); - if (build == null) return null; - var saved = new CraftImage(plugin, build, image).save(); - register(saved); - return saved; - } - - @Override - public @Nullable Image get(World world) { - return images.get(world.getUID()); - } - - @Override - public void register(Image image) { - images.put(image.getWorld().getUID(), image); - } - - @Override - public Image getOrDefault(World world) { - return images.getOrDefault(world.getUID(), new CraftImage(plugin, world)); - } - - @Override - public List findImageFiles() { - var files = Bukkit.getWorldContainer().listFiles(file -> - file.isFile() && file.getName().endsWith(".image")); - return files != null ? Arrays.asList(files) : Collections.emptyList(); - } - - @Override - public List findWorldFiles() { - var files = Bukkit.getWorldContainer().listFiles(file -> - file.isDirectory() && new File(file, "level.dat").isFile()); - return files != null ? Arrays.asList(files) : Collections.emptyList(); - } - - @Override - public List findImages() { - return findImageFiles().stream() - .map(this::of) - .filter(Objects::nonNull) - .toList(); - } - - @Override - public WorldImage createWorldImage() { - return new CraftWorldImage(true); - } - - @Override - @SuppressWarnings("deprecation") - public CraftWorldImage of(World world) { - return new CraftWorldImage( - world.getName(), - world.getKey(), - null, null, null, - world.getEnvironment(), - Objects.requireNonNullElse(world.getWorldType(), WorldType.NORMAL), - world.isAutoSave(), - world.canGenerateStructures(), - world.isHardcore(), - true, - world.getSeed(), - true - ); - } - - @Override - public @Nullable WorldImage of(File file) { - return file.isFile() ? loadFile(IO.of(file), null).getRoot() : null; - } - - public static FileIO loadFile(IO file, @Nullable WorldImage root) { - var gson = new GsonBuilder() - .registerTypeHierarchyAdapter(NamespacedKey.class, KeyAdapter.Bukkit.INSTANCE) - .setPrettyPrinting() - .create(); - return root != null ? new GsonFile<>(file, root, gson) : new GsonFile<>(file, CraftWorldImage.class, gson); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftWorldImage.java b/plugin/src/main/java/net/thenextlvl/worlds/image/CraftWorldImage.java deleted file mode 100644 index f1e40626..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/image/CraftWorldImage.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.thenextlvl.worlds.image; - -import com.google.gson.JsonObject; -import core.io.IO; -import core.nbt.tag.CompoundTag; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; -import net.thenextlvl.worlds.util.WorldReader; -import org.bukkit.*; -import org.bukkit.generator.BiomeProvider; -import org.bukkit.generator.ChunkGenerator; -import org.jetbrains.annotations.Nullable; - -import java.io.File; - -@Getter -@Setter -@AllArgsConstructor -@Accessors(fluent = true, chain = true) -public class CraftWorldImage implements WorldImage { - private String name; - private NamespacedKey key; - private @Nullable JsonObject settings; - private @Nullable Generator generator; - private @Nullable DeletionType deletionType; - private World.Environment environment; - private WorldType worldType; - private boolean autoSave; - private boolean loadOnStart; - - private volatile boolean generateStructures; - private volatile boolean hardcore; - private volatile long seed; - - private volatile boolean skipValidation; - - public CraftWorldImage(boolean skipValidation) { - this.skipValidation = skipValidation; - } - - @Override - public @Nullable World build() { - try { - preValidate(); - } catch (Exception e) { - e.printStackTrace(); - } - var creator = new WorldCreator(name(), key()) - .generator(resolveChunkGenerator()) - .biomeProvider(resolveBiomeProvider()) - .generateStructures(generateStructures()) - .generatorSettings(settings() != null ? settings().toString() : "") - .environment(environment()) - .hardcore(hardcore()) - .type(worldType()) - .seed(seed()); - var world = creator.createWorld(); - if (world != null) world.setAutoSave(autoSave()); - return world; - } - - private void preValidate() { - var worldFolder = new File(Bukkit.getWorldContainer(), name()); - var dataFile = IO.of(worldFolder, "level.dat"); - if (!dataFile.exists()) return; - var reader = new WorldReader(dataFile); - if (!skipValidation) { - reader.generateStructures().ifPresent(this::generateStructures); - reader.hardcore().ifPresent(this::hardcore); - reader.seed().ifPresent(this::seed); - } - var data = reader.file().getRoot().getOrAdd("Data", new CompoundTag()); - var worldGenSettings = data.getOrAdd("WorldGenSettings", new CompoundTag()); - worldGenSettings.add("seed", seed()); - worldGenSettings.add("generate_features", generateStructures()); - data.add("hardcore", hardcore()); - reader.file().save(); - } - - private @Nullable ChunkGenerator resolveChunkGenerator() { - if (generator() == null) return null; - var plugin = Bukkit.getPluginManager().getPlugin(generator().plugin()); - if (plugin == null || !plugin.isEnabled()) throw new IllegalArgumentException(); - return plugin.getDefaultWorldGenerator(name(), generator().id()); - } - - private @Nullable BiomeProvider resolveBiomeProvider() { - if (generator() == null) return null; - var plugin = Bukkit.getPluginManager().getPlugin(generator().plugin()); - if (plugin == null || !plugin.isEnabled()) throw new IllegalArgumentException(); - return plugin.getDefaultBiomeProvider(name(), generator().id()); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/image/Generator.java b/plugin/src/main/java/net/thenextlvl/worlds/image/Generator.java deleted file mode 100644 index d93516c6..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/image/Generator.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.thenextlvl.worlds.image; - -import org.bukkit.World; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.PluginClassLoader; -import org.jetbrains.annotations.Nullable; - -public record Generator(String plugin, @Nullable String id) { - - @Nullable - @SuppressWarnings("UnstableApiUsage") - public static Generator of(World world) { - if (world.getGenerator() == null) return null; - var loader = world.getGenerator().getClass().getClassLoader(); - if (!(loader instanceof PluginClassLoader pluginLoader)) return null; - if (pluginLoader.getPlugin() == null) return null; - return new Generator(pluginLoader.getPlugin().getName(), null); - } - - public static boolean hasChunkGenerator(Class clazz) { - try { - return clazz.getMethod("getDefaultWorldGenerator", String.class, String.class).getDeclaringClass().equals(clazz); - } catch (NoSuchMethodException e) { - return false; - } - } - - public static boolean hasBiomeProvider(Class clazz) { - try { - return clazz.getMethod("getDefaultBiomeProvider", String.class, String.class).getDeclaringClass().equals(clazz); - } catch (NoSuchMethodException e) { - return false; - } - } - - @Override - public String toString() { - return id() != null ? plugin() + ":" + id() : plugin(); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/link/CraftLinkRegistry.java b/plugin/src/main/java/net/thenextlvl/worlds/link/CraftLinkRegistry.java deleted file mode 100644 index 3c823dd6..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/link/CraftLinkRegistry.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.thenextlvl.worlds.link; - -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import core.file.FileIO; -import core.file.format.GsonFile; -import core.io.IO; -import core.paper.adapters.world.WorldAdapter; -import lombok.RequiredArgsConstructor; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.World; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Stream; - -@RequiredArgsConstructor -public class CraftLinkRegistry implements LinkRegistry { - private final Set links = new HashSet<>(); - private final Worlds plugin; - - @Override - public Stream getLinks() { - return links.stream(); - } - - @Override - public boolean isRegistered(Link link) { - return links.contains(link); - } - - @Override - public boolean register(Link link) { - return links.add(link); - } - - @Override - public boolean unregister(Link link) { - return links.remove(link); - } - - @Override - public boolean unregisterAll(World world) { - return links.removeIf(link -> link.source().equals(world) || link.destination().equals(world)); - } - - public void saveLinks() { - var file = loadFile(); - file.setRoot(links); - file.save(); - } - - public void loadLinks() { - var file = loadFile(); - links.addAll(file.getRoot()); - } - - private FileIO> loadFile() { - return new GsonFile<>( - IO.of(plugin.getDataFolder(), "links.json"), - new HashSet<>(), new TypeToken<>() { - }, new GsonBuilder() - .registerTypeHierarchyAdapter(World.class, WorldAdapter.Key.INSTANCE) - .setPrettyPrinting() - .create()); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/listener/PortalListener.java b/plugin/src/main/java/net/thenextlvl/worlds/listener/PortalListener.java index f9be0399..798712f7 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/listener/PortalListener.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/listener/PortalListener.java @@ -2,15 +2,11 @@ import io.papermc.paper.event.entity.EntityPortalReadyEvent; import lombok.RequiredArgsConstructor; -import net.thenextlvl.worlds.Worlds; -import net.thenextlvl.worlds.link.Link; -import net.thenextlvl.worlds.util.PortalCooldown; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.PortalType; -import org.bukkit.World; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.model.PortalCooldown; +import org.bukkit.*; import org.bukkit.block.BlockFace; -import org.bukkit.entity.Player; +import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -23,41 +19,39 @@ @RequiredArgsConstructor public class PortalListener implements Listener { private final PortalCooldown cooldown = new PortalCooldown(); - private final Worlds plugin; + private final WorldsPlugin plugin; @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onEntityPortal(EntityPortalReadyEvent event) { - plugin.linkRegistry().getLinks() - .filter(link -> event.getPortalType().equals(link.portalType())) - .filter(link -> event.getEntity().getWorld().equals(link.source())) - .findFirst() - .map(Link::destination) - .ifPresent(event::setTargetWorld); + if (event.getPortalType().equals(PortalType.CUSTOM)) return; + plugin.linkController().getTarget(event.getEntity().getWorld(), event.getPortalType()) + .map(Bukkit::getWorld).ifPresentOrElse(event::setTargetWorld, () -> + event.setTargetWorld(null)); } @SuppressWarnings("UnstableApiUsage") - @EventHandler(priority = EventPriority.HIGH) + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onEntityPortalEnter(EntityPortalEnterEvent event) { - if (!event.getLocation().getBlock().getType().equals(Material.END_PORTAL)) return; + if (!event.getPortalType().equals(PortalType.ENDER)) return; + + event.setCancelled(true); + if (!cooldown.start(plugin, event.getEntity())) return; + var readyEvent = new EntityPortalReadyEvent(event.getEntity(), null, PortalType.ENDER); - if (!readyEvent.callEvent() || readyEvent.getTargetWorld() == null) return; + onEntityPortal(readyEvent); + + if (readyEvent.getTargetWorld() == null) return; + if (readyEvent.getTargetWorld().getEnvironment().equals(World.Environment.THE_END)) { generateEndPlatform(readyEvent.getTargetWorld()); - var spawn = new Location(readyEvent.getTargetWorld(), 100.5, 50, 0.5, 90, 0); - event.getEntity().teleportAsync(spawn, END_PORTAL); - } else if (readyEvent.getTargetWorld().getEnvironment().equals(World.Environment.NETHER)) { - var spawn = event.getLocation().clone(); - spawn.setWorld(readyEvent.getTargetWorld()); - spawn.setX(spawn.getX() * readyEvent.getTargetWorld().getCoordinateScale()); - spawn.setZ(spawn.getZ() * readyEvent.getTargetWorld().getCoordinateScale()); + var spawn = new Location(readyEvent.getTargetWorld(), 100.5, 49, 0.5, 90, 0); event.getEntity().teleportAsync(spawn, END_PORTAL); - } else if (event.getEntity() instanceof Player player) { - var location = player.getRespawnLocation(); - if (location == null || !location.getWorld().equals(readyEvent.getTargetWorld())) - player.teleportAsync(readyEvent.getTargetWorld().getSpawnLocation(), END_PORTAL); - else player.teleportAsync(location, END_PORTAL); - } else event.getEntity().teleportAsync(readyEvent.getTargetWorld().getSpawnLocation(), END_PORTAL); + } else if (event.getEntity() instanceof CraftPlayer player) { + if (!player.getHandle().seenCredits) player.getHandle().showEndCredits(); + if (player.getRespawnLocation() != null) player.teleportAsync(player.getRespawnLocation(), END_PORTAL); + else player.teleportAsync(readyEvent.getTargetWorld().getSpawnLocation(), END_PORTAL); + } else event.getEntity().teleport(readyEvent.getTargetWorld().getSpawnLocation(), END_PORTAL); } private void generateEndPlatform(World world) { diff --git a/plugin/src/main/java/net/thenextlvl/worlds/listener/ServerListener.java b/plugin/src/main/java/net/thenextlvl/worlds/listener/ServerListener.java new file mode 100644 index 00000000..4c7a3cb4 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/listener/ServerListener.java @@ -0,0 +1,33 @@ +package net.thenextlvl.worlds.listener; + +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.world.WorldLoadEvent; + +@RequiredArgsConstructor +public class ServerListener implements Listener { + private final WorldsPlugin plugin; + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldLoad(WorldLoadEvent event) { + if (!event.getWorld().key().asString().equals("minecraft:overworld")) return; + plugin.levelView().listLevels() + .filter(plugin.levelView()::canLoad) + .forEach(level -> { + try { + var world = plugin.levelView().loadLevel(level); + if (world != null) plugin.getComponentLogger().debug("Loaded dimension {} at {}", + world.key().asString(), level.getPath()); + else plugin.getComponentLogger().error("Failed to load the level {}", level.getPath()); + } catch (Exception e) { + plugin.getComponentLogger().error("An unexpected error occurred while loading the level {}", + level.getPath(), e); + plugin.getComponentLogger().error("Please report the error above on GitHub: {}", + "https://github.com/TheNextLvl-net/worlds/issues/new/choose"); + } + }); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/listener/WorldListener.java b/plugin/src/main/java/net/thenextlvl/worlds/listener/WorldListener.java deleted file mode 100644 index 2cc33c78..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/listener/WorldListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.thenextlvl.worlds.listener; - -import lombok.RequiredArgsConstructor; -import net.thenextlvl.worlds.Worlds; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.world.WorldUnloadEvent; - -@RequiredArgsConstructor -public class WorldListener implements Listener { - private final Worlds plugin; - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onWorldUnload(WorldUnloadEvent event) { - plugin.linkRegistry().unregisterAll(event.getWorld()); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/listener/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/listener/package-info.java new file mode 100644 index 00000000..e4c41ffc --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/listener/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@MethodsReturnNotNullByDefault +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.listener; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/plugin/src/main/java/net/thenextlvl/worlds/util/PortalCooldown.java b/plugin/src/main/java/net/thenextlvl/worlds/model/PortalCooldown.java similarity index 80% rename from plugin/src/main/java/net/thenextlvl/worlds/util/PortalCooldown.java rename to plugin/src/main/java/net/thenextlvl/worlds/model/PortalCooldown.java index d93fbf7d..6f6751dc 100644 --- a/plugin/src/main/java/net/thenextlvl/worlds/util/PortalCooldown.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/model/PortalCooldown.java @@ -1,12 +1,13 @@ -package net.thenextlvl.worlds.util; +package net.thenextlvl.worlds.model; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import org.bukkit.entity.Entity; import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; import java.util.WeakHashMap; -public class PortalCooldown extends WeakHashMap { +public class PortalCooldown extends WeakHashMap { public boolean isActive(Entity entity) { return containsKey(entity); } diff --git a/api/src/main/java/net/thenextlvl/worlds/image/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/model/package-info.java similarity index 89% rename from api/src/main/java/net/thenextlvl/worlds/image/package-info.java rename to plugin/src/main/java/net/thenextlvl/worlds/model/package-info.java index 06f93ad7..03e75f4d 100644 --- a/api/src/main/java/net/thenextlvl/worlds/image/package-info.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/model/package-info.java @@ -1,8 +1,8 @@ @TypesAreNotNullByDefault @FieldsAreNotNullByDefault -@ParametersAreNotNullByDefault @MethodsReturnNotNullByDefault -package net.thenextlvl.worlds.image; +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.model; import core.annotation.FieldsAreNotNullByDefault; import core.annotation.MethodsReturnNotNullByDefault; diff --git a/plugin/src/main/java/net/thenextlvl/worlds/util/WorldReader.java b/plugin/src/main/java/net/thenextlvl/worlds/util/WorldReader.java deleted file mode 100644 index d7d45149..00000000 --- a/plugin/src/main/java/net/thenextlvl/worlds/util/WorldReader.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.thenextlvl.worlds.util; - -import core.annotation.TypesAreNotNullByDefault; -import core.io.IO; -import core.nbt.file.NBTFile; -import core.nbt.tag.CompoundTag; -import lombok.Getter; -import lombok.experimental.Accessors; -import org.bukkit.Bukkit; - -import java.io.File; -import java.util.Optional; -import java.util.OptionalLong; - -@Getter -@Accessors(fluent = true) -@TypesAreNotNullByDefault -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class WorldReader { - private final NBTFile file; - private OptionalLong seed = OptionalLong.empty(); - private Optional generateStructures = Optional.empty(); - private Optional hardcore = Optional.empty(); - - public WorldReader(IO dataFile) { - this.file = new NBTFile<>(dataFile, new CompoundTag()); - - if (!file.getRoot().containsKey("Data")) return; - var data = file.getRoot().getAsCompound("Data"); - - var oldSeed = data.get("RandomSeed"); - if (oldSeed != null) this.seed = OptionalLong.of(oldSeed.getAsLong()); - - var oldStructures = data.get("MapFeatures"); - if (oldStructures != null) this.seed = OptionalLong.of(oldStructures.getAsLong()); - - var hardcore = data.get("hardcore"); - if (hardcore != null) this.hardcore = Optional.of(hardcore.getAsBoolean()); - - if (!data.containsKey("WorldGenSettings")) return; - var worldGenSettings = data.getAsCompound("WorldGenSettings"); - - var seed = worldGenSettings.get("seed"); - if (seed != null) this.seed = OptionalLong.of(seed.getAsLong()); - - var structures = worldGenSettings.get("generate_features"); - if (structures != null) generateStructures = Optional.of(structures.getAsBoolean()); - } - - public WorldReader(String name) { - this(IO.of(new File(Bukkit.getWorldContainer(), name), "level.dat")); - } -} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/version/PluginVersionChecker.java b/plugin/src/main/java/net/thenextlvl/worlds/version/PluginVersionChecker.java new file mode 100644 index 00000000..8b302e5a --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/version/PluginVersionChecker.java @@ -0,0 +1,41 @@ +package net.thenextlvl.worlds.version; + +import core.paper.version.PaperHangarVersionChecker; +import core.version.SemanticVersion; +import lombok.Getter; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +@Getter +@SuppressWarnings("UnstableApiUsage") +public class PluginVersionChecker extends PaperHangarVersionChecker { + private final SemanticVersion versionRunning; + private final Plugin plugin; + + public PluginVersionChecker(Plugin plugin) { + super("Worlds"); + this.plugin = plugin; + this.versionRunning = Objects.requireNonNull(parseVersion(plugin.getPluginMeta().getVersion())); + } + + @Override + public @Nullable SemanticVersion parseVersion(String version) { + return SemanticVersion.parse(version); + } + + public void checkVersion() { + retrieveLatestSupportedVersion(latest -> latest.ifPresentOrElse(version -> { + if (version.equals(getVersionRunning())) { + plugin.getComponentLogger().info("You are running the latest version of Worlds"); + } else if (version.compareTo(getVersionRunning()) > 0) { + plugin.getComponentLogger().warn("An update for Worlds is available"); + plugin.getComponentLogger().warn("You are running version {}, the latest supported version is {}", getVersionRunning(), version); + plugin.getComponentLogger().warn("Update at https://modrinth.com/plugin/worlds-1 or https://hangar.papermc.io/TheNextLvl/Worlds"); + } else { + plugin.getComponentLogger().warn("You are running a snapshot version of Worlds"); + } + }, () -> plugin.getComponentLogger().error("Version check failed"))); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/version/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/version/package-info.java new file mode 100644 index 00000000..dc9aa34d --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/version/package-info.java @@ -0,0 +1,10 @@ +@TypesAreNotNullByDefault +@FieldsAreNotNullByDefault +@MethodsReturnNotNullByDefault +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.version; + +import core.annotation.FieldsAreNotNullByDefault; +import core.annotation.MethodsReturnNotNullByDefault; +import core.annotation.ParametersAreNotNullByDefault; +import core.annotation.TypesAreNotNullByDefault; \ No newline at end of file diff --git a/plugin/src/main/java/net/thenextlvl/worlds/view/PaperLevelView.java b/plugin/src/main/java/net/thenextlvl/worlds/view/PaperLevelView.java new file mode 100644 index 00000000..8152fa3e --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/view/PaperLevelView.java @@ -0,0 +1,293 @@ +package net.thenextlvl.worlds.view; + +import core.io.IO; +import core.io.PathIO; +import core.nbt.file.NBTFile; +import core.nbt.tag.*; +import lombok.RequiredArgsConstructor; +import net.thenextlvl.worlds.WorldsPlugin; +import net.thenextlvl.worlds.api.model.LevelExtras; +import net.thenextlvl.worlds.api.model.WorldPreset; +import net.thenextlvl.worlds.api.preset.Biome; +import net.thenextlvl.worlds.api.preset.Layer; +import net.thenextlvl.worlds.api.preset.Preset; +import net.thenextlvl.worlds.api.preset.Structure; +import net.thenextlvl.worlds.api.view.LevelView; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.WorldType; +import org.bukkit.craftbukkit.CraftWorld; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +public class PaperLevelView implements LevelView { + private final WorldsPlugin plugin; + + @Override + public Stream listLevels() { + return Optional.ofNullable(plugin.getServer().getWorldContainer() + .listFiles(File::isDirectory)).stream() + .flatMap(files -> Arrays.stream(files).filter(this::isLevel)); + } + + @Override + public boolean isLevel(File file) { + return file.isDirectory() && (new File(file, "level.dat").isFile() || new File(file, "level.dat_old").isFile()); + } + + @Override + public boolean hasNetherDimension(File level) { + return new File(level, "DIM-1").isDirectory(); + } + + @Override + public boolean hasEndDimension(File level) { + return new File(level, "DIM1").isDirectory(); + } + + @Override + public boolean canLoad(File level) { + return plugin.getServer().getWorlds().stream() + .map(World::getWorldFolder) + .noneMatch(level::equals); + } + + @Override + public World.Environment getEnvironment(File level) { + var end = hasEndDimension(level); + var nether = hasNetherDimension(level); + if (end && nether) return World.Environment.NORMAL; + if (end) return World.Environment.THE_END; + if (nether) return World.Environment.NETHER; + return World.Environment.NORMAL; + } + + @Override + public @Nullable World loadLevel(File level) { + return loadLevel(level, getEnvironment(level)); + } + + @Override + public @Nullable World loadLevel(File level, @Nullable NamespacedKey key, Predicate> predicate) { + return loadLevel(level, getEnvironment(level), key, predicate); + } + + @Override + public @Nullable World loadLevel(File level, World.Environment environment) { + return loadLevel(level, environment, extras -> extras.map(LevelExtras::enabled).isPresent()); + } + + @Override + public @Nullable World loadLevel(File level, Predicate> predicate) { + return loadLevel(level, getEnvironment(level), predicate); + } + + @Override + public @Nullable World loadLevel(File level, World.Environment environment, Predicate> predicate) { + return loadLevel(level, environment, null, predicate); + } + + @Override + public @Nullable World loadLevel(File level, World.Environment environment, @Nullable NamespacedKey key, Predicate> predicate) { + var data = getLevelDataFile(level).getRoot().optional("Data"); + var extras = data.flatMap(this::getExtras); + + if (!predicate.test(extras)) return null; + + var settings = data.flatMap(tag -> tag.optional("WorldGenSettings")); + var dimensions = settings.flatMap(tag -> tag.optional("dimensions")); + var dimension = dimensions.flatMap(tag -> tag.optional(getDimension(tag, environment))); + var generator = dimension.flatMap(tag -> tag.optional("generator")); + + var worldPreset = generator.flatMap(this::getWorldPreset); + + var generatorSettings = worldPreset.filter(preset -> preset.equals(WorldPreset.FLAT)) + .flatMap(worldType -> generator.flatMap(this::getFlatPreset)); + + var hardcore = data.flatMap(tag -> tag.optional("hardcore")) + .orElseThrow(() -> new NoSuchElementException("hardcore")) + .getAsBoolean(); + var seed = settings.flatMap(tag -> tag.optional("seed")) + .orElseThrow(() -> new NoSuchElementException("seed")) + .getAsInt(); + var structures = settings.flatMap(tag -> tag.optional("generate_features")) + .orElseThrow(() -> new NoSuchElementException("generate_features")) + .getAsBoolean(); + + var worldKey = Optional.ofNullable(key).orElseGet(() -> + extras.map(LevelExtras::key).orElseGet(() -> { + var namespace = level.getName().toLowerCase() + .replace("(", "").replace(")", "") + .replace(" ", "_"); + return new NamespacedKey("worlds", namespace); + })); + + var creator = new WorldCreator(level.getName(), worldKey) + .environment(environment) + .generateStructures(structures) + .hardcore(hardcore) + .seed(seed) + .type(typeOf(worldPreset.orElse(WorldPreset.NORMAL))); + + generatorSettings.ifPresent(preset -> creator.generatorSettings(preset.serialize().toString())); + + return creator.createWorld(); + } + + private WorldType typeOf(WorldPreset worldPreset) { + if (worldPreset.equals(WorldPreset.AMPLIFIED)) return WorldType.AMPLIFIED; + if (worldPreset.equals(WorldPreset.FLAT)) return WorldType.FLAT; + if (worldPreset.equals(WorldPreset.LARGE_BIOMES)) return WorldType.LARGE_BIOMES; + return WorldType.NORMAL; + } + + @Override + public Optional getExtras(CompoundTag data) { + return data.optional("BukkitValues") + .map(Tag::getAsCompound) + .map(values -> { + var key = values.optional("worlds:world_key") + .map(Tag::getAsString) + .map(NamespacedKey::fromString) + .orElse(null); + var enabled = values.optional("worlds:enabled") + .map(Tag::getAsBoolean) + .orElse(true); + return new LevelExtras(key, enabled); + }); + } + + @Override + public Optional getFlatPreset(CompoundTag generator) { + var settings = generator.optional("settings"); + + if (settings.isEmpty()) return Optional.empty(); + + var preset = new Preset(); + + settings.flatMap(tag -> tag.optional("biome")) + .map(Tag::getAsString) + .map(Biome::literal) + .ifPresent(preset::biome); + + settings.flatMap(tag -> tag.optional("features")) + .map(Tag::getAsBoolean) + .ifPresent(preset::features); + + settings.flatMap(tag -> tag.optional("lakes")) + .map(Tag::getAsBoolean) + .ifPresent(preset::lakes); + + settings.flatMap(tag -> tag.>optional("layers")) + .map(tag -> tag.stream().map(layer -> { + var block = layer.optional("block").orElseThrow().getAsString(); + var height = layer.optional("height").orElseThrow().getAsInt(); + return new Layer(block, height); + }).collect(Collectors.toCollection(LinkedHashSet::new))) + .ifPresent(preset::layers); + + settings.flatMap(tag -> tag.optional("structure_overrides") + .filter(Tag::isList).map(Tag::getAsList)) + .map(list -> list.stream() + .map(structure -> new Structure(structure.getAsString())) + .collect(Collectors.toCollection(LinkedHashSet::new))) + .ifPresent(preset::structures); + settings.flatMap(tag -> tag.optional("structure_overrides") + .filter(Tag::isString).map(Tag::getAsString)) + .map(Structure::new) + .ifPresent(preset::addStructure); + + return Optional.of(preset); + } + + @Override + public NBTFile getLevelDataFile(File level) { + return new NBTFile<>(Optional.of( + IO.of(level, "level.dat") + ).filter(PathIO::exists).orElseGet(() -> + IO.of(level, "level.dat_old") + ), new CompoundTag()); + } + + @Override + public String getDimension(CompoundTag dimensions, World.Environment environment) { + return switch (environment) { + case NORMAL -> "minecraft:overworld"; + case NETHER -> "minecraft:the_nether"; + case THE_END -> "minecraft:the_end"; + case CUSTOM -> dimensions.keySet().stream().filter(s -> !s.startsWith("minecraft")).findAny() + .orElseThrow(() -> new UnsupportedOperationException("Could not find custom dimension")); + }; + } + + @Override + public Optional getWorldPreset(CompoundTag generator) { + + var settings = getGeneratorSettings(generator); + if (settings.filter(s -> s.equals(WorldPreset.LARGE_BIOMES.key().asString())).isPresent()) + return Optional.of(WorldPreset.LARGE_BIOMES); + if (settings.filter(s -> s.equals(WorldPreset.AMPLIFIED.key().asString())).isPresent()) + return Optional.of(WorldPreset.AMPLIFIED); + + var type = generator.optional("biome_source") + .flatMap(tag -> tag.optional("type")) + .map(Tag::getAsString); + + if (type.filter(s -> s.equals(WorldPreset.SINGLE_BIOME.key().asString())).isPresent()) + return Optional.of(WorldPreset.SINGLE_BIOME); + if (type.filter(s -> s.equals(WorldPreset.CHECKERBOARD.key().asString())).isPresent()) + return Optional.of(WorldPreset.CHECKERBOARD); + + var generatorType = getGeneratorType(generator); + if (generatorType.filter(s -> s.equals(WorldPreset.DEBUG.key().asString())).isPresent()) + return Optional.of(WorldPreset.DEBUG); + if (generatorType.filter(s -> s.equals(WorldPreset.FLAT.key().asString())).isPresent()) + return Optional.of(WorldPreset.FLAT); + if (generatorType.filter(s -> s.equals(WorldPreset.NORMAL.key().asString())).isPresent()) + return Optional.of(WorldPreset.NORMAL); + + return Optional.empty(); + } + + @Override + public Optional getGeneratorSettings(CompoundTag generator) { + return generator.optional("settings").filter(Tag::isString).map(Tag::getAsString); + } + + @Override + public Optional getGeneratorType(CompoundTag generator) { + return generator.optional("type").map(Tag::getAsString); + } + + @Override + public void saveLevel(World world, boolean flush) { + var level = ((CraftWorld) world).getHandle(); + var oldSave = level.noSave; + level.noSave = false; + level.save(null, flush, false); + level.noSave = oldSave; + } + + @Override + public void saveLevelData(World world, boolean async) { + var level = ((CraftWorld) world).getHandle(); + if (level.getDragonFight() != null) { + level.serverLevelData.setEndDragonFightData(level.getDragonFight().saveData()); + } + level.getChunkSource().getDataStorage().save(async); + + level.serverLevelData.setWorldBorder(level.getWorldBorder().createSettings()); + level.serverLevelData.setCustomBossEvents(level.getServer().getCustomBossEvents().save(level.registryAccess())); + level.convertable.saveDataTag(level.getServer().registryAccess(), level.serverLevelData, level.getServer().getPlayerList().getSingleplayerData()); + } +} diff --git a/plugin/src/main/java/net/thenextlvl/worlds/view/PluginGeneratorView.java b/plugin/src/main/java/net/thenextlvl/worlds/view/PluginGeneratorView.java new file mode 100644 index 00000000..58c82a08 --- /dev/null +++ b/plugin/src/main/java/net/thenextlvl/worlds/view/PluginGeneratorView.java @@ -0,0 +1,29 @@ +package net.thenextlvl.worlds.view; + +import net.thenextlvl.worlds.api.view.GeneratorView; +import org.bukkit.plugin.Plugin; + +public class PluginGeneratorView implements GeneratorView { + @Override + public boolean hasGenerator(Plugin plugin) { + return hasChunkGenerator(plugin.getClass()) || hasBiomeProvider(plugin.getClass()); + } + + @Override + public boolean hasChunkGenerator(Class clazz) { + try { + return clazz.getMethod("getDefaultWorldGenerator", String.class, String.class).getDeclaringClass().equals(clazz); + } catch (NoSuchMethodException e) { + return false; + } + } + + @Override + public boolean hasBiomeProvider(Class clazz) { + try { + return clazz.getMethod("getDefaultBiomeProvider", String.class, String.class).getDeclaringClass().equals(clazz); + } catch (NoSuchMethodException e) { + return false; + } + } +} diff --git a/api/src/main/java/net/thenextlvl/worlds/link/package-info.java b/plugin/src/main/java/net/thenextlvl/worlds/view/package-info.java similarity index 89% rename from api/src/main/java/net/thenextlvl/worlds/link/package-info.java rename to plugin/src/main/java/net/thenextlvl/worlds/view/package-info.java index dc8bea85..a1c0fbb6 100644 --- a/api/src/main/java/net/thenextlvl/worlds/link/package-info.java +++ b/plugin/src/main/java/net/thenextlvl/worlds/view/package-info.java @@ -1,8 +1,8 @@ @TypesAreNotNullByDefault @FieldsAreNotNullByDefault -@ParametersAreNotNullByDefault @MethodsReturnNotNullByDefault -package net.thenextlvl.worlds.link; +@ParametersAreNotNullByDefault +package net.thenextlvl.worlds.view; import core.annotation.FieldsAreNotNullByDefault; import core.annotation.MethodsReturnNotNullByDefault; diff --git a/plugin/src/main/resources/worlds.properties b/plugin/src/main/resources/worlds.properties index aeff2c64..943d1621 100644 --- a/plugin/src/main/resources/worlds.properties +++ b/plugin/src/main/resources/worlds.properties @@ -1,47 +1,58 @@ +command.confirmation= '>Confirm your '>action, this cannot be undone! +command.sender= You cannot use this command +environment.end=End Environment +environment.nether=Nether Environment +environment.normal=Normal Environment prefix=Worlds » -world.save.success= Saved the world -world.save.failed= Failed to save the world -world.create.success= Successfully created the world +world.clone.failed= Failed to clone world +world.clone.success= Successfully cloned world world.create.failed= Failed to create the world -world.import.success= Successfully imported the world +world.create.success= Successfully created the world +world.delete.disallowed= The overworld can only be scheduled for deletion +world.delete.failed= Failed to delete the world +world.delete.scheduled= The world will be deleted on the next restart +world.delete.success= Successfully deleted the world world.import.failed= Failed to import the world -world.list= Worlds (): -world.list.hover=Click to teleport to -world.teleport.player.self= You got teleported to -world.teleport.player.other= Teleported to -world.known= A world called does already exist -world.unknown= A world called does not exist -world.preset.invalid= The world preset is not a valid json string -world.preset.flat= Presets are only applicable on flat maps -world.clone.success= Successfully cloned world -world.clone.failed= Failed to clone world -world.spawn.set.success= Set world spawn at , , [] -world.spawn.set.failed= Failed to change world spawn -world.info.name= Name: -world.info.players= Players: -world.info.type= Type: -world.info.environment= Environment: +world.import.success= Successfully imported the world +world.info.dimension= Dimension: world.info.generator= Generator: +world.info.name= Name: () +world.info.players= Players: world.info.seed= Seed: '>'> -world.delete.disallowed= The world can only be scheduled for deletion -world.delete.success= Successfully deleted the world +world.info.type= Type: () +world.link.failed= Failed to link and +world.link.list.empty= There are no links yet +world.link.list= Links (): +world.link.success= Linked and +world.list.hover=Click to teleport to +world.list= Worlds (): +world.load.failed= Failed to load the world +world.load.success= Successfully loaded the world +world.regenerate.disallowed= The overworld can only be scheduled for regeneration +world.regenerate.failed= Failed to regenerate the world +world.regenerate.scheduled= The world will be regenerated on the next restart +world.regenerate.success= Successfully regenerated the world +world.save.already-off= Saving is already turned off +world.save.already-on= Saving is already turned on +world.save.failed= +world.save.off= Automatic saving is now disabled +world.save.on= Automatic saving is now enabled +world.save.saving= +world.save.success= +world.save= Saving the world (this may take a moment!) +world.spawn.set.failed= Failed to change world spawn +world.spawn.set.success= Set world spawn at , , [] +world.teleport.none= No entity was found +world.teleport.other= Teleported to +world.teleport.others= Teleported entities to +world.teleport.self= You got teleported to +world.type.amplified=Amplified +world.type.flat=Superflat +world.type.large_biomes=Large biomes +world.type.normal=Default +world.unlink.failed= Failed to unlink from +world.unlink.success= Unlinked from +world.unload.disallowed= The overworld cannot be unloaded world.unload.failed= Failed to unload the world -world.unload.success= Successfully unloaded the world -world.delete.nothing= There is nothing to delete -world.delete.failed= Failed to delete the world -world.delete.scheduled= The world will be deleted on the next restart -image.delete.failed= Failed to delete the image -link.exists= The link : -> does already exists -link.exists.not= The link does not exist -link.deleted= Deleted the link : -> -link.created= Created a new link : -> -link.list.empty= There are no links yet -link.list= Links (): -image.exists.not= An image called does not exist -environment.custom= Cannot generate world using environment custom -player.unknown= The player is not online -command.permission= You have no rights () -command.sender= You cannot use this command -command.argument= Invalid command argument -command.flag.combination= You can't combine the flag with -command.confirmation= '>Confirm your '>action, this cannot be undone! \ No newline at end of file +world.unload.fallback= The fallback and target world cannot match +world.unload.success= Successfully unloaded the world \ No newline at end of file diff --git a/plugin/src/main/resources/worlds_german.properties b/plugin/src/main/resources/worlds_german.properties index 0cdd53fc..9f5b26bb 100644 --- a/plugin/src/main/resources/worlds_german.properties +++ b/plugin/src/main/resources/worlds_german.properties @@ -1,46 +1,55 @@ -world.save.success= Die Welt wurde gespeichert -world.save.failed= Die Welt konnte nicht gespeichert werden -world.create.success= Die Welt wurde erfolgreich erstellt +command.confirmation= Bestätige deine '>Eingabe mit '> Diese Aktion ist unwiderruflich! +command.sender= Du kannst diesen command nicht nutzen +environment.end=Endumgebung +environment.nether=Nether Umgebung +environment.normal=Normale Umgebung +world.clone.failed= Die Welt konnte nicht geklont werden +world.clone.success= Die Welt wurde erfolgreich geklont world.create.failed= Die Welt konnte nicht erstellt werden -world.import.success= Die Welt wurde erfolgreich importiert +world.create.success= Die Welt wurde erfolgreich erstellt +world.delete.disallowed= Die Oberwelt kann nur zum Löschen eingeplant werden +world.delete.failed= Die Welt konnte nicht gelöscht werden +world.delete.scheduled= Die Welt wird beim nächsten Neustart gelöscht +world.delete.success= Die Welt wurde erfolgreich gelöscht world.import.failed= Die Welt konnte nicht importiert werden -world.list= Welten (): -world.list.hover=Klicke um dich zu zu teleportieren -world.teleport.player.self= Du wurdest zu teleportiert -world.teleport.player.other= Du hast zu teleportiert -world.known= Eine Welt mit dem namen existiert bereits -world.unknown= Eine Welt mit dem namen existiert nicht -world.preset.invalid= Die Welten Voreinstellung ist kein gültiger json Text -world.preset.flat= Voreinstellungen sind nur auf flache Welten anwendbar -world.clone.success= Die Welt wurde erfolgreich geklont -world.clone.failed= Die Welt konnte nicht geklont werden -world.spawn.set.success= Der spawn ist jetzt bei , , [] -world.spawn.set.failed= Der spawn konnte nicht neu gesetzt werden -world.info.name= Name: -world.info.players= Spieler: -world.info.type= Typ: -world.info.environment= Umfeld: +world.import.success= Die Welt wurde erfolgreich importiert +world.info.dimension= Dimension: world.info.generator= Generator: +world.info.name= Name: () +world.info.players= Spieler: world.info.seed= Startwert: '>'> -world.name.absent= Du musst eine Welt angeben -world.delete.disallowed= Die Welt kann nur zum Löschen eingeplant werden -world.delete.success= Die Welt wurde erfolgreich gelöscht +world.info.type= Typ: () +world.link.failed= und konnten nicht verbunden werden +world.link.list.empty= Es existieren noch keine links +world.link.list= Links (): +world.link.success= und sind jetzt verbunden +world.list.hover=Klicke um dich zu zu teleportieren +world.list= Welten (): +world.load.failed= Die Welt konnte nicht geladen werden +world.load.success= Die Welt wurde erfolgreich geladen +world.regenerate.disallowed= Die Oberwelt kann nur zur Regeneration eingeplant werden +world.regenerate.failed= Die Welt konnte nicht regeneriert werden +world.regenerate.scheduled= Die Welt wird beim nächsten Neustart regeneriert +world.regenerate.success= Die Welt wurde erfolgreich regeneriert +world.save.already-off= Automatisches Speichern ist bereits deaktiviert +world.save.already-on= Automatisches Speichern ist bereits aktiviert +world.save.off= Automatisches Speichern ist jetzt deaktiviert +world.save.on= Automatisches Speichern ist jetzt aktiviert +world.save.success= Die Welt wurde gespeichert +world.save= Die Welt wird gespeichert (das kann einen Moment dauern!) +world.spawn.set.failed= Der Welteinstiegspunk konnte nicht neu gesetzt werden +world.spawn.set.success= Der Welteinstiegspunk ist jetzt bei , , [] +world.teleport.none= Es wurde kein Objekt gefunden +world.teleport.other= wurde zu teleportiert +world.teleport.others= Objekte wurden zu teleportiert +world.teleport.self= Du wurdest zu teleportiert +world.type.amplified=Erweitert +world.type.flat=Superflach +world.type.large_biomes=Große Biome +world.type.normal=Standard +world.unlink.failed= konnte nicht von getrennt werden +world.unlink.success= wurde von getrennt +world.unload.disallowed= Die Oberwelt kann nicht entladen werden world.unload.failed= Die Welt konnte nicht entladen werden -world.unload.success= Die Welt wurde erfolgreich entladen -world.delete.failed= Die Welt konnte nicht gelöscht werden -world.delete.scheduled= Die Welt wird beim nächsten Neustart gelöscht -image.delete.failed= Das Abbild konnte nicht gelöscht werden -link.exists= Der Link : -> existiert bereits -link.exists.not= Der Link existiert nicht -link.deleted= Der Link : -> wurde gelöscht -link.created= Der Link : -> wurde erstellt -link.list.empty= Es existieren noch keine links -link.list= Links (): -image.exists.not= Ein Abbild mit dem namen existiert nicht -environment.custom= Custom kann nicht als umwelt genutzt werden -player.unknown= Der Spieler ist nicht online -command.permission= Darauf hast du keine rechte () -command.sender= Du kannst diesen command nicht nutzen -command.argument= Ungültiges command Argument -command.flag.combination= Du kannst und nicht kombinieren -command.confirmation= Bestätige deine '>Eingabe mit '> Diese Aktion ist unwiderruflich! \ No newline at end of file +world.unload.fallback= Die zu löschende und Rückfallwelt können nicht die gleiche sein +world.unload.success= Die Welt wurde erfolgreich entladen \ No newline at end of file