diff --git a/verification/src/changes/accepted-core-public-api-changes.json b/verification/src/changes/accepted-core-public-api-changes.json index 2d0bde7e9c..8400941833 100644 --- a/verification/src/changes/accepted-core-public-api-changes.json +++ b/verification/src/changes/accepted-core-public-api-changes.json @@ -122,6 +122,13 @@ "changes": [ "METHOD_NEW_DEFAULT" ] + }, + { + "type": "com.sk89q.worldedit.world.World", + "member": "Method com.sk89q.worldedit.world.World.generateFeature(com.sk89q.worldedit.world.generation.ConfiguredFeatureType,com.sk89q.worldedit.EditSession,com.sk89q.worldedit.math.BlockVector3)", + "changes": [ + "METHOD_NEW_DEFAULT" + ] } ], "LazyReference was never publicly extensible": [ diff --git a/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightAdapter.java index 009ab2da02..077949fce6 100644 --- a/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightAdapter.java @@ -27,6 +27,7 @@ import com.google.common.util.concurrent.Futures; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Lifecycle; +import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; @@ -63,6 +64,7 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.entity.EntityTypes; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.Util; import net.minecraft.core.BlockPos; @@ -92,6 +94,7 @@ import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.RandomSource; import net.minecraft.util.StringRepresentable; import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.world.Clearable; @@ -104,6 +107,7 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelSettings; +import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -116,6 +120,7 @@ import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.dimension.LevelStem; import net.minecraft.world.level.levelgen.WorldOptions; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.PrimaryLevelData; import net.minecraft.world.phys.BlockHitResult; @@ -188,6 +193,8 @@ public final class PaperweightAdapter implements BukkitImplAdapter { private final Field chunkProviderExecutorField; private final Watchdog watchdog; + private static final RandomSource random = RandomSource.create(); + // ------------------------------------------------------------------------ // Code that may break between versions of Minecraft // ------------------------------------------------------------------------ @@ -320,6 +327,21 @@ public OptionalInt getInternalBlockStateId(BlockState state) { return combinedId == 0 && state.getBlockType() != BlockTypes.AIR ? OptionalInt.empty() : OptionalInt.of(combinedId); } + public BlockState adapt(net.minecraft.world.level.block.state.BlockState blockState) { + int internalId = Block.getId(blockState); + BlockState state = BlockStateIdAccess.getBlockStateById(internalId); + if (state == null) { + state = BukkitAdapter.adapt(CraftBlockData.createData(blockState)); + } + + return state; + } + + public net.minecraft.world.level.block.state.BlockState adapt(BlockState blockState) { + int internalId = BlockStateIdAccess.getBlockStateId(blockState); + return Block.stateById(internalId); + } + @Override public BlockState getBlock(Location location) { checkNotNull(location); @@ -333,14 +355,7 @@ public BlockState getBlock(Location location) { LevelChunk chunk = handle.getChunk(x >> 4, z >> 4); final BlockPos blockPos = new BlockPos(x, y, z); final net.minecraft.world.level.block.state.BlockState blockData = chunk.getBlockState(blockPos); - int internalId = Block.getId(blockData); - BlockState state = BlockStateIdAccess.getBlockStateById(internalId); - if (state == null) { - org.bukkit.block.Block bukkitBlock = location.getBlock(); - state = BukkitAdapter.adapt(bukkitBlock.getBlockData()); - } - - return state; + return adapt(blockData); } @Override @@ -873,6 +888,21 @@ public void initializeRegistries() { BiomeType.REGISTRY.register(name.toString(), new BiomeType(name.toString())); } } + + // Features + for (ResourceLocation name: server.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).keySet()) { + if (ConfiguredFeatureType.REGISTRY.get(name.toString()) == null) { + ConfiguredFeatureType.REGISTRY.register(name.toString(), new ConfiguredFeatureType(name.toString())); + } + } + } + + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + ConfiguredFeature k = originalWorld.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).get(ResourceLocation.tryParse(type.getId())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + WorldGenLevel proxyLevel = PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this); + return k != null && k.place(proxyLevel, chunkManager.getGenerator(), random, new BlockPos(pt.getBlockX(), pt.getBlockY(), pt.getBlockZ())); } // ------------------------------------------------------------------------ diff --git a/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightServerLevelDelegateProxy.java b/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightServerLevelDelegateProxy.java new file mode 100644 index 0000000000..ee899df47d --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1.19.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_19_R2/PaperweightServerLevelDelegateProxy.java @@ -0,0 +1,120 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.bukkit.adapter.impl.v1_19_R2; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BlockTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +public class PaperweightServerLevelDelegateProxy implements InvocationHandler { + + private final EditSession editSession; + private final ServerLevel serverLevel; + private final PaperweightAdapter adapter; + + private PaperweightServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) { + this.editSession = editSession; + this.serverLevel = serverLevel; + this.adapter = adapter; + } + + public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) { + return (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + new PaperweightServerLevelDelegateProxy(editSession, serverLevel, adapter) + ); + } + + @Nullable + private BlockEntity getBlockEntity(BlockPos blockPos) { + BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); + if (tileEntity == null) { + return null; + } + BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos)); + newEntity.load((CompoundTag) adapter.fromNative(this.editSession.getFullBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ())).getNbtReference().getValue())); + + return newEntity; + } + + private BlockState getBlockState(BlockPos blockPos) { + return adapter.adapt(this.editSession.getBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()))); + } + + private boolean setBlock(BlockPos blockPos, BlockState blockState) { + try { + return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), adapter.adapt(blockState)); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + try { + return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), BlockTypes.AIR.getDefaultState()); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "a_", "getBlockState" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + // getBlockState + return getBlockState(blockPos); + } + } + case "c_", "getBlockEntity" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + // getBlockEntity + return getBlockEntity(blockPos); + } + } + case "a", "setBlock", "removeBlock", "destroyBlock" -> { + if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof BlockState blockState) { + // setBlock + return setBlock(blockPos, blockState); + } else if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof Boolean bl) { + // removeBlock (and also matches destroyBlock) + return removeBlock(blockPos, bl); + } + } + default -> { } + } + + return method.invoke(this.serverLevel, args); + } + +} diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java index 4e8592ece3..df9db2e58b 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitWorld.java @@ -44,6 +44,7 @@ import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.weather.WeatherType; import com.sk89q.worldedit.world.weather.WeatherTypes; import io.papermc.lib.PaperLib; @@ -435,6 +436,16 @@ public boolean canPlaceAt(BlockVector3 position, com.sk89q.worldedit.world.block return true; } + @Override + public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); + if (adapter != null) { + return adapter.generateFeature(type, getWorld(), editSession, position); + } + // No adapter, we can't generate this. + return false; + } + private static volatile boolean hasWarnedImplError = false; @Override diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java index 3d08909250..afbbc7f543 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/adapter/BukkitImplAdapter.java @@ -19,6 +19,7 @@ package com.sk89q.worldedit.bukkit.adapter; +import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; @@ -36,6 +37,7 @@ import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemType; import org.bukkit.Location; import org.bukkit.World; @@ -254,6 +256,7 @@ default boolean clearContainerBlockContents(World world, BlockVector3 pt) { } /** +<<<<<<< HEAD * Checks if this adapter supports custom biomes. * @return if custom biomes are supported */ @@ -279,6 +282,19 @@ default BiomeType getBiome(Location location) { throw new UnsupportedOperationException("This adapter does not support custom biomes."); } + /** + * Generates a Minecraft feature at the given location. + * + * @param feature The feature + * @param world The world + * @param session The EditSession + * @param pt The location + * @return If it succeeded + */ + default boolean generateFeature(ConfiguredFeatureType feature, World world, EditSession session, BlockVector3 pt) { + throw new UnsupportedOperationException("This adapter does not support generating features."); + } + /** * Initialize registries that require NMS access. */ diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java index 0446a11d11..38bbd93eb9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -24,6 +24,7 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.command.argument.HeightConverter; +import com.sk89q.worldedit.command.factory.FeatureGeneratorFactory; import com.sk89q.worldedit.command.factory.ReplaceFactory; import com.sk89q.worldedit.command.factory.TreeGeneratorFactory; import com.sk89q.worldedit.command.tool.BrushTool; @@ -78,6 +79,7 @@ import com.sk89q.worldedit.util.formatting.text.format.TextColor; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; @@ -546,6 +548,24 @@ public void forest(Player player, LocalSession localSession, new Paint(new TreeGeneratorFactory(type), density / 100), shape, "worldedit.brush.forest"); } + @Command( + name = "feature", + desc = "Feature brush, paints Minecraft generation features" + ) + @CommandPermissions("worldedit.brush.feature") + public void feature(Player player, LocalSession localSession, + @Arg(desc = "The shape of the region") + RegionFactory shape, + @Arg(desc = "The size of the brush", def = "5") + double radius, + @Arg(desc = "The density of the brush", def = "5") + double density, + @Arg(desc = "The type of feature to use") + ConfiguredFeatureType type) throws WorldEditException { + setOperationBasedBrush(player, localSession, radius, + new Paint(new FeatureGeneratorFactory(type), density / 100), shape, "worldedit.brush.feature"); + } + @Command( name = "raise", desc = "Raise brush, raise all blocks by one" diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java index c1f769d859..491942fb00 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java @@ -39,6 +39,7 @@ import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; @@ -235,6 +236,23 @@ public int pumpkins(Actor actor, LocalSession session, EditSession editSession, return affected; } + @Command( + name = "/feature", + desc = "Generate Minecraft features" + ) + @CommandPermissions("worldedit.generation.feature") + @Logging(POSITION) + public int feature(Actor actor, LocalSession session, EditSession editSession, + @Arg(desc = "The feature") + ConfiguredFeatureType feature) throws WorldEditException { + if (editSession.getWorld().generateFeature(feature, editSession, session.getPlacementPosition(actor))) { + actor.printInfo(TranslatableComponent.of("worldedit.feature.created")); + } else { + actor.printError(TranslatableComponent.of("worldedit.feature.failed")); + } + return 0; + } + @Command( name = "/hpyramid", desc = "Generate a hollow pyramid" diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/RegistryConverter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/RegistryConverter.java index ad07203a66..38edcec64a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/RegistryConverter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/RegistryConverter.java @@ -32,6 +32,7 @@ import com.sk89q.worldedit.world.fluid.FluidCategory; import com.sk89q.worldedit.world.fluid.FluidType; import com.sk89q.worldedit.world.gamemode.GameMode; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.weather.WeatherType; @@ -62,7 +63,8 @@ public static void register(CommandManager commandManager) { FluidType.class, FluidCategory.class, GameMode.class, - WeatherType.class + WeatherType.class, + ConfiguredFeatureType.class ) .stream() .map(c -> (Class) c) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/FeatureGeneratorFactory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/FeatureGeneratorFactory.java new file mode 100644 index 0000000000..6434f016bb --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/FeatureGeneratorFactory.java @@ -0,0 +1,44 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.command.factory; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.function.Contextual; +import com.sk89q.worldedit.function.EditContext; +import com.sk89q.worldedit.function.generator.FeatureGenerator; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; + +public final class FeatureGeneratorFactory implements Contextual { + private final ConfiguredFeatureType type; + + public FeatureGeneratorFactory(ConfiguredFeatureType type) { + this.type = type; + } + + @Override + public FeatureGenerator createFromContext(EditContext input) { + return new FeatureGenerator((EditSession) input.getDestination(), type); + } + + @Override + public String toString() { + return "feature of type " + type; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/FeatureGenerator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/FeatureGenerator.java new file mode 100644 index 0000000000..6c49138db9 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/FeatureGenerator.java @@ -0,0 +1,52 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.function.generator; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.function.RegionFunction; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; + +/** + * Generates forests by searching for the ground starting from the given upper Y + * coordinate for every column given. + */ +public class FeatureGenerator implements RegionFunction { + + private final ConfiguredFeatureType featureType; + private final EditSession editSession; + + /** + * Create a new instance. + * + * @param editSession the edit session + * @param featureType the feature type + */ + public FeatureGenerator(EditSession editSession, ConfiguredFeatureType featureType) { + this.editSession = editSession; + this.featureType = featureType; + } + + @Override + public boolean apply(BlockVector3 position) throws WorldEditException { + return editSession.getWorld().generateFeature(featureType, editSession, position); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java index 276b206ff9..bfbb3f7fdd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/World.java @@ -41,6 +41,7 @@ import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockStateHolder; import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.weather.WeatherType; import java.nio.file.Path; @@ -273,6 +274,18 @@ default boolean regenerate(Region region, Extent extent, RegenOptions options) { */ boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException; + /** + * Generate a feature at the given position. + * + * @param type The feature type + * @param editSession The {@link EditSession} + * @param position The position + * @return True if the generation was successful + */ + default boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + return false; + } + /** * Load the chunk at the given position if it isn't loaded. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/ConfiguredFeatureType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/ConfiguredFeatureType.java new file mode 100644 index 0000000000..56888acd98 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/ConfiguredFeatureType.java @@ -0,0 +1,43 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.world.generation; + +import com.sk89q.worldedit.registry.Keyed; +import com.sk89q.worldedit.registry.NamespacedRegistry; + +public class ConfiguredFeatureType implements Keyed { + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("configured feature type"); + + private final String id; + + public ConfiguredFeatureType(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String toString() { + return this.id; + } +} diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index 84921ef860..2d328da7fc 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -245,6 +245,8 @@ "worldedit.sphere.created": "{0} blocks have been created.", "worldedit.forestgen.created": "{0} trees created.", "worldedit.pumpkins.created": "{0} pumpkin patches created.", + "worldedit.feature.created": "Feature created.", + "worldedit.feature.failed": "Failed to generate feature. Is it a valid spot for it?", "worldedit.pyramid.created": "{0} blocks have been created.", "worldedit.generate.created": "{0} blocks have been created.", "worldedit.generatebiome.changed": "{0} biomes affected.", diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java index a4bfa0f365..f63fd6b72a 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java @@ -31,6 +31,7 @@ import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; @@ -47,6 +48,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.phys.Vec3; @@ -196,6 +198,15 @@ public static BlockState adapt(net.minecraft.world.level.block.state.BlockState return worldEdit; } + public static BaseBlock adapt(BlockEntity blockEntity) { + int blockStateId = Block.getId(blockEntity.getBlockState()); + BlockState worldEdit = BlockStateIdAccess.getBlockStateById(blockStateId); + if (worldEdit == null) { + worldEdit = FabricTransmogrifier.transmogToWorldEdit(blockEntity.getBlockState()); + } + return worldEdit.toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(blockEntity.saveWithId()))); + } + public static Block adapt(BlockType blockType) { return FabricWorldEdit.getRegistry(Registries.BLOCK).get(new ResourceLocation(blockType.getId())); } diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorld.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorld.java index fa811ef8e9..d42c1642cb 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorld.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorld.java @@ -34,6 +34,7 @@ import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.fabric.internal.ExtendedMinecraftServer; +import com.sk89q.worldedit.fabric.internal.FabricServerLevelDelegateProxy; import com.sk89q.worldedit.fabric.internal.FabricWorldNativeAccess; import com.sk89q.worldedit.fabric.internal.NBTConverter; import com.sk89q.worldedit.fabric.mixin.AccessorDerivedLevelData; @@ -60,6 +61,7 @@ import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.weather.WeatherType; import com.sk89q.worldedit.world.weather.WeatherTypes; @@ -84,6 +86,7 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -462,12 +465,21 @@ public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 if (type == TreeType.CHORUS_PLANT) { position = position.add(0, 1, 0); } + WorldGenLevel proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world); return generator != null && generator.place( - world, chunkManager.getGenerator(), random, + proxyLevel, chunkManager.getGenerator(), random, FabricAdapter.toBlockPos(position) ); } + public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = (ServerLevel) getWorld(); + ConfiguredFeature k = world.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).get(ResourceLocation.tryParse(type.getId())); + ServerChunkCache chunkManager = world.getChunkSource(); + WorldGenLevel proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world); + return k != null && k.place(proxyLevel, chunkManager.getGenerator(), random, FabricAdapter.toBlockPos(position)); + } + @Override public void checkLoadedChunk(BlockVector3 pt) { getWorld().getChunk(FabricAdapter.toBlockPos(pt)); diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java index a4045c0cfb..5747cb4865 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java @@ -40,6 +40,7 @@ import com.sk89q.worldedit.world.block.BlockCategory; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.entity.EntityType; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; import net.fabricmc.api.ModInitializer; @@ -228,6 +229,12 @@ private void setupRegistries(MinecraftServer server) { ItemCategory.REGISTRY.register(name.toString(), new ItemCategory(name.toString())); } }); + // Features + for (ResourceLocation name: server.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).keySet()) { + if (ConfiguredFeatureType.REGISTRY.get(name.toString()) == null) { + ConfiguredFeatureType.REGISTRY.register(name.toString(), new ConfiguredFeatureType(name.toString())); + } + } } private void onStartingServer(MinecraftServer minecraftServer) { diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java new file mode 100644 index 0000000000..07ee8d81c5 --- /dev/null +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java @@ -0,0 +1,117 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.fabric.internal; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.fabric.FabricAdapter; +import com.sk89q.worldedit.world.block.BlockTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +public class FabricServerLevelDelegateProxy implements InvocationHandler { + + private final EditSession editSession; + private final ServerLevel serverLevel; + + private FabricServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel) { + this.editSession = editSession; + this.serverLevel = serverLevel; + } + + public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel) { + return (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + new FabricServerLevelDelegateProxy(editSession, serverLevel) + ); + } + + @Nullable + private BlockEntity getBlockEntity(BlockPos blockPos) { + BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); + if (tileEntity == null) { + return null; + } + BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos)); + newEntity.load(NBTConverter.toNative(this.editSession.getFullBlock(FabricAdapter.adapt(blockPos)).getNbtReference().getValue())); + + return newEntity; + } + + private BlockState getBlockState(BlockPos blockPos) { + return FabricAdapter.adapt(this.editSession.getBlock(FabricAdapter.adapt(blockPos))); + } + + private boolean setBlock(BlockPos blockPos, BlockState blockState) { + try { + return editSession.setBlock(FabricAdapter.adapt(blockPos), FabricAdapter.adapt(blockState)); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + try { + return editSession.setBlock(FabricAdapter.adapt(blockPos), BlockTypes.AIR.getDefaultState()); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "getBlockState", "method_8320" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + return getBlockState(blockPos); + } + } + case "getBlockEntity", "method_8321" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + return getBlockEntity(blockPos); + } + } + case "setBlock", "method_8652" -> { + if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof BlockState blockState) { + return setBlock(blockPos, blockState); + } + } + case "removeBlock", "destroyBlock", "method_8650", "method_8651" -> { + if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof Boolean bl) { + return removeBlock(blockPos, bl); + } + } + default -> { } + } + + return method.invoke(this.serverLevel, args); + } + +} diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorld.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorld.java index 4d6049ba3a..c9584405aa 100644 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorld.java +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorld.java @@ -33,6 +33,7 @@ import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.forge.internal.ForgeServerLevelDelegateProxy; import com.sk89q.worldedit.forge.internal.ForgeWorldNativeAccess; import com.sk89q.worldedit.forge.internal.NBTConverter; import com.sk89q.worldedit.forge.internal.TileEntityUtils; @@ -57,6 +58,7 @@ import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.weather.WeatherType; import com.sk89q.worldedit.world.weather.WeatherTypes; @@ -81,6 +83,7 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -447,11 +450,20 @@ public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 if (type == TreeType.CHORUS_PLANT) { position = position.add(0, 1, 0); } + WorldGenLevel levelProxy = ForgeServerLevelDelegateProxy.newInstance(editSession, world); return generator != null && generator.place( - world, chunkManager.getGenerator(), random, ForgeAdapter.toBlockPos(position) + levelProxy, chunkManager.getGenerator(), random, ForgeAdapter.toBlockPos(position) ); } + public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = getWorld(); + ConfiguredFeature k = world.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).get(ResourceLocation.tryParse(type.getId())); + ServerChunkCache chunkManager = world.getChunkSource(); + WorldGenLevel levelProxy = ForgeServerLevelDelegateProxy.newInstance(editSession, world); + return k != null && k.place(levelProxy, chunkManager.getGenerator(), random, ForgeAdapter.toBlockPos(position)); + } + @Override public void checkLoadedChunk(BlockVector3 pt) { getWorld().getChunk(ForgeAdapter.toBlockPos(pt)); diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java index 9d5eb57a81..ac27beb5da 100644 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java @@ -42,6 +42,7 @@ import com.sk89q.worldedit.world.block.BlockCategory; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.entity.EntityType; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.commands.CommandSourceStack; @@ -208,6 +209,12 @@ private void setupRegistries(MinecraftServer server) { ItemCategory.REGISTRY.register(name.toString(), new ItemCategory(name.toString())); } }); + // Features + for (ResourceLocation name: server.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).keySet()) { + if (ConfiguredFeatureType.REGISTRY.get(name.toString()) == null) { + ConfiguredFeatureType.REGISTRY.register(name.toString(), new ConfiguredFeatureType(name.toString())); + } + } } @SubscribeEvent diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/internal/ForgeServerLevelDelegateProxy.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/internal/ForgeServerLevelDelegateProxy.java new file mode 100644 index 0000000000..ff9e64d4e8 --- /dev/null +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/internal/ForgeServerLevelDelegateProxy.java @@ -0,0 +1,117 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.forge.internal; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.forge.ForgeAdapter; +import com.sk89q.worldedit.world.block.BlockTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +public class ForgeServerLevelDelegateProxy implements InvocationHandler { + + private final EditSession editSession; + private final ServerLevel serverLevel; + + private ForgeServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel) { + this.editSession = editSession; + this.serverLevel = serverLevel; + } + + public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel) { + return (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + new ForgeServerLevelDelegateProxy(editSession, serverLevel) + ); + } + + @Nullable + private BlockEntity getBlockEntity(BlockPos blockPos) { + BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); + if (tileEntity == null) { + return null; + } + BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos)); + newEntity.load(NBTConverter.toNative(this.editSession.getFullBlock(ForgeAdapter.adapt(blockPos)).getNbtReference().getValue())); + + return newEntity; + } + + private BlockState getBlockState(BlockPos blockPos) { + return ForgeAdapter.adapt(this.editSession.getBlock(ForgeAdapter.adapt(blockPos))); + } + + private boolean setBlock(BlockPos blockPos, BlockState blockState) { + try { + return editSession.setBlock(ForgeAdapter.adapt(blockPos), ForgeAdapter.adapt(blockState)); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + try { + return editSession.setBlock(ForgeAdapter.adapt(blockPos), BlockTypes.AIR.getDefaultState()); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "getBlockState", "m_8055_" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + return getBlockState(blockPos); + } + } + case "getBlockEntity", "m_7702_" -> { + if (args.length == 1 && args[0] instanceof BlockPos blockPos) { + return getBlockEntity(blockPos); + } + } + case "setBlock", "m_7731_" -> { + if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof BlockState blockState) { + return setBlock(blockPos, blockState); + } + } + case "removeBlock", "destroyBlock", "m_7471_", "m_7740_" -> { + if (args.length >= 2 && args[0] instanceof BlockPos blockPos && args[1] instanceof Boolean bl) { + return removeBlock(blockPos, bl); + } + } + default -> { } + } + + return method.invoke(this.serverLevel, args); + } + +}