diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java index 28391ee583..ce2332b925 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R2/PaperweightAdapter.java @@ -68,10 +68,12 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; @@ -113,6 +115,9 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -918,6 +923,20 @@ public void initializeRegistries() { } } + // Trees + HolderLookup.RegistryLookup placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + placedFeatureRegistry.listElements() + .filter(feature -> { + var underlyingFeature = feature.value().feature().value().feature(); + return underlyingFeature instanceof TreeFeature || underlyingFeature instanceof CoralTreeFeature; + }) + .forEach(feature -> { + String key = feature.key().toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + }); + // BiomeCategories Registry biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME); biomeRegistry.getTagNames().forEach(tagKey -> { diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java index dadd3f1139..46d7d0b93e 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweAdapter.java @@ -19,6 +19,7 @@ import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -51,6 +52,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -88,6 +90,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -679,6 +682,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed //FAWE end } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .registryOrThrow(Registries.PLACED_FEATURE) + .get(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getList()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R3/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R3/PaperweightAdapter.java index f524676068..f4562764bc 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R3/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_20_R3/PaperweightAdapter.java @@ -68,10 +68,12 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; @@ -113,6 +115,9 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -917,6 +922,20 @@ public void initializeRegistries() { } } + // Trees + HolderLookup.RegistryLookup placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + placedFeatureRegistry.listElements() + .filter(feature -> { + var underlyingFeature = feature.value().feature().value().feature(); + return underlyingFeature instanceof TreeFeature || underlyingFeature instanceof CoralTreeFeature; + }) + .forEach(feature -> { + String key = feature.key().toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + }); + // BiomeCategories Registry biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME); biomeRegistry.getTagNames().forEach(tagKey -> { diff --git a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweAdapter.java index c622b25cb9..3b69d37248 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweAdapter.java @@ -19,6 +19,7 @@ import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -50,6 +51,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -87,6 +89,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -678,6 +681,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed //FAWE end } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .registryOrThrow(Registries.PLACED_FEATURE) + .get(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getList()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext.fawe/v1_20_R4/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext.fawe/v1_20_R4/PaperweightAdapter.java index cb6da03c34..47785f4a9e 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext.fawe/v1_20_R4/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext.fawe/v1_20_R4/PaperweightAdapter.java @@ -68,10 +68,12 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; @@ -117,6 +119,9 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -941,6 +946,20 @@ public void initializeRegistries() { } } + // Trees + HolderLookup.RegistryLookup placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + placedFeatureRegistry.listElements() + .filter(feature -> { + var underlyingFeature = feature.value().feature().value().feature(); + return underlyingFeature instanceof TreeFeature || underlyingFeature instanceof CoralTreeFeature; + }) + .forEach(feature -> { + String key = feature.key().toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + }); + // BiomeCategories Registry biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME); biomeRegistry.getTagNames().forEach(tagKey -> { diff --git a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweAdapter.java index 44bb4c6ec5..3526440b07 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweAdapter.java @@ -20,6 +20,7 @@ import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -52,6 +53,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -92,6 +94,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -692,6 +695,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed //FAWE end } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .registryOrThrow(Registries.PLACED_FEATURE) + .get(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getList()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_R1/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_R1/PaperweightAdapter.java index 5e8ed03d7e..6f6cd435a2 100644 --- a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_R1/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_R1/PaperweightAdapter.java @@ -29,6 +29,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; @@ -68,11 +69,13 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderSet; import net.minecraft.core.Registry; import net.minecraft.core.component.DataComponentPatch; @@ -117,6 +120,10 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -927,6 +934,20 @@ public void initializeRegistries() { } } + // Trees + HolderLookup.RegistryLookup placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + placedFeatureRegistry.listElements() + .filter(feature -> { + var underlyingFeature = feature.value().feature().value().feature(); + return underlyingFeature instanceof TreeFeature || underlyingFeature instanceof CoralTreeFeature; + }) + .forEach(feature -> { + String key = feature.key().toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + }); + // BiomeCategories Registry biomeRegistry = server.registryAccess().registryOrThrow(Registries.BIOME); biomeRegistry.getTagNames().forEach(tagKey -> { @@ -957,6 +978,16 @@ public void sendBiomeUpdates(World world, Iterable chunks) { } @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature k = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE) + .getOrThrow(ResourceKey.create(Registries.PLACED_FEATURE, ResourceLocation.tryParse(treeType.id()))) + .value(); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + WorldGenLevel proxyLevel = PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this); + return k != null && k.place(proxyLevel, chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + 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.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweAdapter.java index 033d2983c0..e6a542a607 100644 --- a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweAdapter.java @@ -20,6 +20,7 @@ import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -52,6 +53,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -92,6 +94,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -693,6 +696,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed //FAWE end } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .registryOrThrow(Registries.PLACED_FEATURE) + .get(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getList()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_11/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_11/PaperweightAdapter.java index 57e5a6cc8a..f1d65c8da8 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_11/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_11/PaperweightAdapter.java @@ -70,6 +70,7 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; @@ -130,6 +131,10 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -902,6 +907,19 @@ public void initializeRegistries() { } } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (Identifier name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + // BiomeCategories Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); biomeRegistry.getTags().forEach(tag -> { @@ -920,6 +938,17 @@ public void initializeRegistries() { }); } + @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(Identifier.tryParse(treeType.id())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + return feature != null && feature.place(proxyLevel.level(), chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + } + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(Identifier.tryParse(type.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweAdapter.java index 77c772a452..c9bc10e856 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweAdapter.java @@ -18,6 +18,7 @@ import com.mojang.serialization.Codec; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -50,6 +51,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -90,6 +92,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -659,6 +662,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed return placeFeatureIntoSession(editSession, populator, placed); } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .lookupOrThrow(Registries.PLACED_FEATURE) + .getValue(Identifier.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getSnapshotBlocks()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_4/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_4/PaperweightAdapter.java index 01987f18cb..70fbe27937 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_4/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_4/PaperweightAdapter.java @@ -65,6 +65,7 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.Util; @@ -112,6 +113,9 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -879,6 +883,19 @@ public void initializeRegistries() { } } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + // BiomeCategories Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); biomeRegistry.getTags().forEach(tag -> { @@ -908,6 +925,17 @@ public void sendBiomeUpdates(World world, Iterable chunks) { originalWorld.getChunkSource().chunkMap.resendBiomesForChunks(nativeChunks); } + @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(treeType.id())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + return feature != null && feature.place(proxyLevel.level(), chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + } + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweAdapter.java index f1282259a1..5aeba33a96 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweAdapter.java @@ -18,6 +18,7 @@ import com.google.common.collect.Sets; import com.mojang.serialization.Codec; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -50,6 +51,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -91,6 +93,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -671,6 +674,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed return placeFeatureIntoSession(editSession, populator, placed); } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .lookupOrThrow(Registries.PLACED_FEATURE) + .getValue(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getList()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_5/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_5/PaperweightAdapter.java index 7dfb944b6c..0d95e3fa40 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_5/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_5/PaperweightAdapter.java @@ -66,6 +66,7 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.Util; @@ -126,6 +127,9 @@ 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.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -891,6 +895,19 @@ public void initializeRegistries() { } } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + // BiomeCategories Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); biomeRegistry.getTags().forEach(tag -> { @@ -909,6 +926,17 @@ public void initializeRegistries() { }); } + @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(treeType.id())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + return feature != null && feature.place(proxyLevel.level(), chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + } + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweAdapter.java index d9be2690fc..9db33d44a3 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweAdapter.java @@ -19,6 +19,7 @@ import com.mojang.serialization.Codec; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -51,6 +52,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -91,6 +93,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -665,6 +668,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed return placeFeatureIntoSession(editSession, populator, placed); } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .lookupOrThrow(Registries.PLACED_FEATURE) + .getValue(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getSnapshotBlocks()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_6/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_6/PaperweightAdapter.java index 20acb6a704..eb3b0b6d20 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_6/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_6/PaperweightAdapter.java @@ -69,6 +69,7 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.Util; @@ -130,6 +131,10 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -914,6 +919,19 @@ public void initializeRegistries() { } } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + // BiomeCategories Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); biomeRegistry.getTags().forEach(tag -> { @@ -932,6 +950,17 @@ public void initializeRegistries() { }); } + @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(treeType.id())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + return feature != null && feature.place(proxyLevel.level(), chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + } + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweAdapter.java index 7c663390c0..a09c96b7a8 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweAdapter.java @@ -19,6 +19,7 @@ import com.mojang.serialization.Codec; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -51,6 +52,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -91,6 +93,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -681,6 +684,44 @@ public boolean canTransformBlocks() { return placeFeatureIntoSession(editSession, populator, placed); } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .lookupOrThrow(Registries.PLACED_FEATURE) + .getValue(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getSnapshotBlocks()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_9/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_9/PaperweightAdapter.java index 17898f34f4..78b86d485b 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_9/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/ext/fawe/v1_21_9/PaperweightAdapter.java @@ -69,6 +69,7 @@ import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.SharedConstants; import net.minecraft.Util; @@ -129,6 +130,10 @@ 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.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -911,6 +916,19 @@ public void initializeRegistries() { } } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + // BiomeCategories Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); biomeRegistry.getTags().forEach(tag -> { @@ -929,6 +947,17 @@ public void initializeRegistries() { }); } + @Override + public boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + ServerLevel originalWorld = ((CraftWorld) world).getHandle(); + PlacedFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(treeType.id())); + ServerChunkCache chunkManager = originalWorld.getChunkSource(); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + return feature != null && feature.place(proxyLevel.level(), chunkManager.getGenerator(), random, new BlockPos(pt.x(), pt.y(), pt.z())); + } + } + public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); diff --git a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweAdapter.java index 2bde85f600..e39b875476 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweAdapter.java @@ -19,6 +19,7 @@ import com.mojang.serialization.Codec; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; @@ -51,6 +52,7 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import io.papermc.lib.PaperLib; @@ -91,6 +93,7 @@ import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureStart; @@ -675,6 +678,44 @@ public boolean generateStructure(StructureType type, World world, EditSession ed return placeFeatureIntoSession(editSession, populator, placed); } + @Override + public boolean generateTree( + final TreeType treeType, + final World world, + final EditSession session, + final BlockVector3 pt + ) throws MaxChangedBlocksException { + ServerLevel serverLevel = getServerLevel(world); + ChunkGenerator generator = serverLevel.getMinecraftWorld().getChunkSource().getGenerator(); + + PlacedFeature placedFeature = serverLevel + .registryAccess() + .lookupOrThrow(Registries.PLACED_FEATURE) + .getValue(ResourceLocation.tryParse(treeType.id())); + + FaweBlockStateListPopulator populator = new FaweBlockStateListPopulator(serverLevel); + List placed = TaskManager.taskManager().sync(() -> { + preCaptureStates(serverLevel); + try { + if (!placedFeature.place( + populator, + generator, + serverLevel.random, + new BlockPos(pt.x(), pt.y(), pt.z()) + )) { + return null; + } + List placedBlocks = new ArrayList<>(populator.getSnapshotBlocks()); + placedBlocks.addAll(serverLevel.capturedBlockStates.values()); + return placedBlocks; + } finally { + postCaptureBlockStates(serverLevel); + } + }); + + return placeFeatureIntoSession(session, populator, placed); + } + private boolean placeFeatureIntoSession( final EditSession editSession, final FaweBlockStateListPopulator populator, diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/FaweAdapter.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/FaweAdapter.java index b902696d2f..66a229fdb0 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/FaweAdapter.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/FaweAdapter.java @@ -13,7 +13,6 @@ import org.bukkit.World; import org.bukkit.block.BlockState; -import java.util.Collection; import java.util.List; import java.util.Map; @@ -35,6 +34,11 @@ protected FaweAdapter(final BukkitImplAdapter parent) { this.parent = parent; } + @Override + public void initializeRegistries() { + parent.initializeRegistries(); + } + @Override public boolean generateTree( final TreeGenerator.TreeType treeType, 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 99aaefdd12..40121aa912 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 @@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; @@ -315,10 +316,16 @@ public boolean clearContainerBlockContents(BlockVector3 pt) { /** * An EnumMap that stores which WorldEdit TreeTypes apply to which Bukkit TreeTypes. */ + @Deprecated private static final EnumMap treeTypeMapping = new EnumMap<>(TreeGenerator.TreeType.class); static { + generateTreeMap(); + } + + @SuppressWarnings("deprecation") + private static void generateTreeMap() { for (TreeGenerator.TreeType type : TreeGenerator.TreeType.values()) { try { TreeType bukkitType = TreeType.valueOf(type.name()); @@ -348,10 +355,13 @@ public boolean clearContainerBlockContents(BlockVector3 pt) { } } + @Deprecated public static TreeType toBukkitTreeType(TreeGenerator.TreeType type) { return treeTypeMapping.get(type); } + @SuppressWarnings("deprecation") + @Deprecated @Override public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 pt) { //FAWE start - allow tree commands to be undone and obey region restrictions @@ -360,6 +370,20 @@ public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession //FAWE end } + @Override + public boolean generateTree( + final com.sk89q.worldedit.world.generation.TreeType type, + final EditSession editSession, + final BlockVector3 position + ) throws MaxChangedBlocksException { + BukkitImplAdapter adapter = WorldEditPlugin.getInstance().getBukkitImplAdapter(); + if (adapter != null) { + return adapter.generateTree(type, getWorld(), editSession, position); + } + // No adapter, we can't generate this. + return false; + } + @Override public void dropItem(Vector3 pt, BaseItemStack item) { World world = getWorld(); diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java index d0708887b0..14b5d019b4 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java @@ -259,7 +259,6 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new AsyncTabCompleteListener(), this); } - initializeRegistries(); // this creates the objects matching Bukkit's enums - but doesn't fill them with data yet if (Bukkit.getWorlds().isEmpty()) { setupPreWorldData(); // register this so we can load world-dependent data right as the first world is loading @@ -291,6 +290,7 @@ public void onEnable() { private void setupPreWorldData() { loadAdapter(); + initializeRegistries(); // this creates the objects matching Bukkit's enums - but doesn't fill them with data yet WorldEdit.getInstance().loadMappings(); } @@ -352,6 +352,13 @@ private void initializeRegistries() { EntityType.REGISTRY.register("minecraft:" + lowerCaseMcId, new EntityType("minecraft:" + lowerCaseMcId)); } } + + // Registries only available via NMS + BukkitImplAdapter adapter = getBukkitImplAdapter(); + if (adapter != null) { + adapter.initializeRegistries(); + } + // ... :| GameModes.get(""); WeatherTypes.get(""); 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 f3db4cb9f3..2fe28534b6 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 @@ -31,6 +31,7 @@ import com.sk89q.jnbt.LinBusConverter; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.bukkit.BukkitAdapter; @@ -53,6 +54,7 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.registry.BlockMaterial; import org.bukkit.Keyed; @@ -330,6 +332,19 @@ default void sendBiomeUpdates(World world, Iterable chunks) { } + /** + * Generates a Minecraft tree at the given location. + * + * @param treeType The tree + * @param world The world + * @param session The EditSession + * @param pt The location + * @return If it succeeded + */ + default boolean generateTree(TreeType treeType, World world, EditSession session, BlockVector3 pt) throws MaxChangedBlocksException { + throw new UnsupportedOperationException("This adapter does not support generating features."); + } + /** * Generates a Minecraft feature at the given location. * diff --git a/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/schematic/ClipboardWorld.java b/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/schematic/ClipboardWorld.java index 1502fdbf38..e838baee4a 100644 --- a/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/schematic/ClipboardWorld.java +++ b/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/schematic/ClipboardWorld.java @@ -41,13 +41,13 @@ import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; -import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.world.AbstractWorld; import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.biome.BiomeType; 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.TreeType; import javax.annotation.Nullable; import java.io.File; @@ -141,8 +141,7 @@ public boolean regenerate(Region region, Extent extent, RegenOptions options) { } @Override - public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) - throws MaxChangedBlocksException { + public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { return false; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/wrappers/WorldWrapper.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/wrappers/WorldWrapper.java index 8421df551c..bd443ff478 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/wrappers/WorldWrapper.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/wrappers/WorldWrapper.java @@ -39,6 +39,7 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.weather.WeatherType; import javax.annotation.Nullable; @@ -290,6 +291,12 @@ public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession } } + @Override + public boolean generateTree(final TreeType type, final EditSession editSession, final BlockVector3 position) throws + MaxChangedBlocksException { + return parent.generateTree(type, editSession, position); + } + @Override public boolean generateStructure(final StructureType type, final EditSession editSession, final BlockVector3 position) { return parent.generateStructure(type, editSession, position); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java index 29c3e26032..330d2b6678 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -150,6 +150,7 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.registry.LegacyMapper; import org.apache.logging.log4j.Logger; @@ -3033,7 +3034,9 @@ public int makePumpkinPatches(BlockVector3 position, int apothem, double density * @param treeType the tree type * @return number of trees created * @throws MaxChangedBlocksException thrown if too many blocks are changed + * @deprecated Use {@link #makeForest(Region, double, TreeType)}. */ + @Deprecated public int makeForest(BlockVector3 basePosition, int size, double density, TreeGenerator.TreeType treeType) throws MaxChangedBlocksException { return makeForest(CuboidRegion.fromCenter(basePosition, size), density, treeType); @@ -3047,7 +3050,9 @@ public int makeForest(BlockVector3 basePosition, int size, double density, TreeG * @param treeType the tree type * @return number of trees created * @throws MaxChangedBlocksException thrown if too many blocks are changed + * @deprecated Use {@link #makeForest(Region, double, TreeType)}. */ + @Deprecated public int makeForest(Region region, double density, TreeGenerator.TreeType treeType) throws MaxChangedBlocksException { ForestGenerator generator = new ForestGenerator(this, treeType); GroundFunction ground = new GroundFunction(new ExistingBlockMask(this), generator); @@ -3059,6 +3064,38 @@ public int makeForest(Region region, double density, TreeGenerator.TreeType tree return ground.getAffected(); } + /** + * Makes a forest. + * + * @param basePosition a position + * @param size a size + * @param density between 0 and 1, inclusive + * @param treeType the tree type + * @return number of trees created + * @throws MaxChangedBlocksException thrown if too many blocks are changed + */ + public int makeForest(BlockVector3 basePosition, int size, double density, TreeType treeType) throws MaxChangedBlocksException { + return makeForest(CuboidRegion.fromCenter(basePosition, size), density, treeType); + } + + /** + * Makes a forest. + * + * @param region the region to generate trees in + * @param density between 0 and 1, inclusive + * @param treeType the tree type + * @return number of trees created + * @throws MaxChangedBlocksException thrown if too many blocks are changed + */ + public int makeForest(Region region, double density, TreeType treeType) throws MaxChangedBlocksException { + com.sk89q.worldedit.function.generator.TreeGenerator generator = new com.sk89q.worldedit.function.generator.TreeGenerator(this, treeType); + GroundFunction ground = new GroundFunction(new ExistingBlockMask(this), generator); + LayerVisitor visitor = new LayerVisitor(asFlatRegion(region), minimumBlockY(region), maximumBlockY(region), ground); + visitor.setMask(new NoiseFilter2D(new RandomNoise(), density)); + Operations.completeLegacy(visitor); + return ground.getAffected(); + } + /** * Get the block distribution inside a region. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ApplyBrushCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ApplyBrushCommands.java index 66e801ae2d..ba5f095a84 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ApplyBrushCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ApplyBrushCommands.java @@ -40,8 +40,8 @@ import com.sk89q.worldedit.internal.command.CommandRegistrationHandler; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.regions.factory.RegionFactory; -import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.world.generation.TreeType; import org.enginehub.piston.CommandManager; import org.enginehub.piston.CommandManagerService; import org.enginehub.piston.CommandParameters; @@ -115,7 +115,7 @@ public void forest( CommandParameters parameters, Player player, LocalSession localSession, @Arg(desc = "The type of tree to plant") - TreeGenerator.TreeType type + TreeType type ) throws WorldEditException { setApplyBrush(parameters, player, localSession, new TreeGeneratorFactory(type)); } 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 e1a2e3e401..1042932e9b 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 @@ -117,7 +117,6 @@ import com.sk89q.worldedit.regions.factory.SphereRegionFactory; import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.util.HandSide; -import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; @@ -128,6 +127,7 @@ import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import org.anarres.parallelgzip.ParallelGZIPOutputStream; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; @@ -1189,7 +1189,7 @@ public void forest( @Arg(desc = "The density of the brush", def = "20") double density, @Arg(desc = "The type of tree to use") - TreeGenerator.TreeType type + TreeType type ) throws WorldEditException { setOperationBasedBrush(player, localSession, radius, new Paint(new TreeGeneratorFactory(type), density / 100), shape, "worldedit.brush.forest" 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 3206524baf..d949859837 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 @@ -53,12 +53,12 @@ import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.util.TreeGenerator.TreeType; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/PaintBrushCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/PaintBrushCommands.java index c34c66055a..f2f276cde0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/PaintBrushCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/PaintBrushCommands.java @@ -39,9 +39,9 @@ import com.sk89q.worldedit.internal.annotation.Direction; import com.sk89q.worldedit.internal.command.CommandRegistrationHandler; import com.sk89q.worldedit.regions.factory.RegionFactory; -import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; +import com.sk89q.worldedit.world.generation.TreeType; import org.enginehub.piston.CommandManager; import org.enginehub.piston.CommandManagerService; import org.enginehub.piston.CommandParameters; @@ -130,7 +130,7 @@ public void forest( CommandParameters parameters, Player player, LocalSession localSession, @Arg(desc = "The type of tree to plant") - TreeGenerator.TreeType type + TreeType type ) throws WorldEditException { setPaintBrush(parameters, player, localSession, new TreeGeneratorFactory(type)); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java index 197968bb36..57432cae58 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java @@ -68,13 +68,13 @@ import com.sk89q.worldedit.regions.RegionOperationException; import com.sk89q.worldedit.regions.Regions; import com.sk89q.worldedit.util.Location; -import com.sk89q.worldedit.util.TreeGenerator.TreeType; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockStateHolder; import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.generation.TreeType; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; import org.enginehub.piston.annotation.param.Arg; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolCommands.java index 1e63b947e5..83e712e3e1 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolCommands.java @@ -51,7 +51,6 @@ import com.sk89q.worldedit.internal.command.CommandRegistrationHandler; import com.sk89q.worldedit.internal.command.CommandUtil; import com.sk89q.worldedit.util.HandSide; -import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.event.ClickEvent; @@ -59,6 +58,7 @@ import com.sk89q.worldedit.world.block.BlockStateHolder; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import org.enginehub.piston.CommandManager; import org.enginehub.piston.CommandManagerService; import org.enginehub.piston.CommandMetadata; @@ -242,7 +242,7 @@ public void inspectBrush(Player player, LocalSession session) throws WorldEditEx public void tree( Player player, LocalSession session, @Arg(desc = "Type of tree to generate", def = "tree") - TreeGenerator.TreeType type + TreeType type ) throws WorldEditException { setTool(player, session, new TreePlanter(type), "worldedit.tool.tree.equip"); } 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 c9b69cc30c..2c4107a386 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 @@ -34,6 +34,7 @@ import com.sk89q.worldedit.world.gamemode.GameMode; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.weather.WeatherType; @@ -66,7 +67,8 @@ public static void register(CommandManager commandManager) { GameMode.class, WeatherType.class, ConfiguredFeatureType.class, - StructureType.class + StructureType.class, + TreeType.class ) .stream() .map(c -> (Class) c) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/TreeGeneratorFactory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/TreeGeneratorFactory.java index 1e6b06d7bc..421b5c675e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/TreeGeneratorFactory.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/factory/TreeGeneratorFactory.java @@ -22,20 +22,20 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.function.Contextual; import com.sk89q.worldedit.function.EditContext; -import com.sk89q.worldedit.function.generator.ForestGenerator; -import com.sk89q.worldedit.util.TreeGenerator; +import com.sk89q.worldedit.function.generator.TreeGenerator; +import com.sk89q.worldedit.world.generation.TreeType; -public final class TreeGeneratorFactory implements Contextual { +public final class TreeGeneratorFactory implements Contextual { - private final TreeGenerator.TreeType type; + private final TreeType type; - public TreeGeneratorFactory(TreeGenerator.TreeType type) { + public TreeGeneratorFactory(TreeType type) { this.type = type; } @Override - public ForestGenerator createFromContext(EditContext input) { - return new ForestGenerator((EditSession) input.getDestination(), type); + public TreeGenerator createFromContext(EditContext input) { + return new TreeGenerator((EditSession) input.getDestination(), type); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/TreePlanter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/TreePlanter.java index db92f6af91..e5740dcb5a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/TreePlanter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/TreePlanter.java @@ -30,7 +30,7 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; -import com.sk89q.worldedit.util.TreeGenerator; +import com.sk89q.worldedit.world.generation.TreeType; import javax.annotation.Nullable; @@ -39,9 +39,9 @@ */ public class TreePlanter implements BlockTool { - private final TreeGenerator.TreeType treeType; + private final TreeType treeType; - public TreePlanter(TreeGenerator.TreeType treeType) { + public TreePlanter(TreeType treeType) { this.treeType = treeType; } @@ -66,7 +66,7 @@ public boolean actPrimary( final BlockVector3 pos = clicked.toVector().add(0, 1, 0).toBlockPoint(); for (int i = 0; i < 10; i++) { - if (treeType.generate(editSession, pos)) { + if (player.getWorld().generateTree(treeType, editSession, pos)) { successful = true; break; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/ForestGenerator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/ForestGenerator.java index a042fd16c1..72b763cd18 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/ForestGenerator.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/ForestGenerator.java @@ -31,7 +31,10 @@ /** * Generates forests by searching for the ground starting from the given upper Y * coordinate for every column given. + * + * @deprecated Use {@link com.sk89q.worldedit.function.generator.TreeGenerator} instead. */ +@Deprecated public class ForestGenerator implements RegionFunction { private final TreeGenerator.TreeType treeType; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/TreeGenerator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/TreeGenerator.java new file mode 100644 index 0000000000..35809a9187 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/generator/TreeGenerator.java @@ -0,0 +1,48 @@ +/* + * 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.TreeType; + +public final class TreeGenerator implements RegionFunction { + + private final TreeType treeType; + private final EditSession editSession; + + /** + * Create a new instance. + * + * @param editSession the edit session + * @param treeType the tree type + */ + public TreeGenerator(EditSession editSession, TreeType treeType) { + this.editSession = editSession; + this.treeType = treeType; + } + + @Override + public boolean apply(BlockVector3 position) throws WorldEditException { + return editSession.getWorld().generateTree(treeType, editSession, position); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/TreeGenerator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/TreeGenerator.java index 2b6bae6b73..68ce6f4c1e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/TreeGenerator.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/TreeGenerator.java @@ -41,8 +41,10 @@ /** * Tree generator. */ +@Deprecated public final class TreeGenerator { + @Deprecated public enum TreeType { TREE("Oak tree", "oak", "tree", "regular"), BIG_TREE("Large oak tree", "largeoak", "bigoak", "big", "bigtree"), diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/NullWorld.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/NullWorld.java index d3ad7bfd92..3832f50a29 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/NullWorld.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/NullWorld.java @@ -38,7 +38,6 @@ import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; -import com.sk89q.worldedit.util.TreeGenerator.TreeType; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeTypes; import com.sk89q.worldedit.world.block.BaseBlock; @@ -143,7 +142,7 @@ public boolean regenerate(Region region, EditSession editSession) { } @Override - public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { + public boolean generateTree(com.sk89q.worldedit.world.generation.TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { return false; } 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 8c9efb5bda..2430e705f8 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 @@ -48,6 +48,7 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.weather.WeatherType; import javax.annotation.Nullable; @@ -309,9 +310,24 @@ default boolean regenerate(Region region, Extent extent, RegenOptions options) { * @param position the position * @return true if generation was successful * @throws MaxChangedBlocksException thrown if too many blocks were changed + * @deprecated Use {@link #generateTree(TreeType, EditSession, BlockVector3)} instead */ - boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) throws - MaxChangedBlocksException; + @Deprecated + default boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) throws + MaxChangedBlocksException { + return false; + } + + /** + * Generate a tree at the given position. + * + * @param type the tree type + * @param editSession the {@link EditSession} + * @param position the position + * @return true if generation was successful + * @throws MaxChangedBlocksException thrown if too many blocks were changed + */ + boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException; /** * Generate a structure at the given position diff --git a/worldedit-bukkit/src/test/java/com/sk89q/worldedit/bukkit/BukkitWorldTest.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/TreeType.java similarity index 65% rename from worldedit-bukkit/src/test/java/com/sk89q/worldedit/bukkit/BukkitWorldTest.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/TreeType.java index df6b33b7b7..27dfd646c6 100644 --- a/worldedit-bukkit/src/test/java/com/sk89q/worldedit/bukkit/BukkitWorldTest.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/generation/TreeType.java @@ -17,19 +17,17 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.bukkit; +package com.sk89q.worldedit.world.generation; -import com.sk89q.worldedit.util.TreeGenerator; -import org.junit.jupiter.api.Test; +import com.sk89q.worldedit.registry.Keyed; +import com.sk89q.worldedit.registry.NamespacedRegistry; -import static org.junit.jupiter.api.Assertions.assertNotNull; +public record TreeType(String id) implements Keyed { -public class BukkitWorldTest { + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("tree_type", "minecraft"); - public void testTreeTypeMapping() { - for (TreeGenerator.TreeType type : TreeGenerator.TreeType.values()) { - assertNotNull(BukkitWorld.toBukkitTreeType(type), "No mapping for: " + type); - } + @Override + public String toString() { + return this.id; } - } 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 e93ae036ef..9f8ea90c44 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 @@ -19,14 +19,14 @@ package com.sk89q.worldedit.fabric; -import static com.google.common.base.Preconditions.checkNotNull; - import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; -import com.google.common.io.Files; -import com.sk89q.jnbt.CompoundTag; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.Futures; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.WorldEditException; @@ -34,102 +34,123 @@ import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; 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.FabricEntity; +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.function.mask.AbstractExtentMask; +import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.internal.Constants; -import com.sk89q.worldedit.internal.block.BlockStateIdAccess; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; -import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; -import com.sk89q.worldedit.util.TreeGenerator.TreeType; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.util.io.file.SafeFiles; import com.sk89q.worldedit.world.AbstractWorld; +import com.sk89q.worldedit.world.RegenOptions; import com.sk89q.worldedit.world.biome.BiomeType; 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.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.weather.WeatherType; import com.sk89q.worldedit.world.weather.WeatherTypes; -import net.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.block.LeavesBlock; -import net.minecraft.block.entity.BlockEntity; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.ItemEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemUsageContext; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.placement.EndPlacements; +import net.minecraft.data.worldgen.placement.TreePlacements; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.WorldGenerationProgressListener; -import net.minecraft.server.world.ServerChunkManager; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.ActionResult; -import net.minecraft.util.Clearable; -import net.minecraft.util.Hand; -import net.minecraft.util.hit.BlockHitResult; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.World; -import net.minecraft.world.WorldSaveHandler; -import net.minecraft.world.biome.DefaultBiomeFeatures; -import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.ChunkManager; -import net.minecraft.world.chunk.ChunkStatus; -import net.minecraft.world.chunk.WorldChunk; -import net.minecraft.world.gen.feature.BirchTreeFeature; -import net.minecraft.world.gen.feature.DarkOakTreeFeature; -import net.minecraft.world.gen.feature.DefaultFeatureConfig; -import net.minecraft.world.gen.feature.Feature; -import net.minecraft.world.gen.feature.FeatureConfig; -import net.minecraft.world.gen.feature.HugeBrownMushroomFeature; -import net.minecraft.world.gen.feature.HugeRedMushroomFeature; -import net.minecraft.world.gen.feature.JungleGroundBushFeature; -import net.minecraft.world.gen.feature.JungleTreeFeature; -import net.minecraft.world.gen.feature.LargeOakTreeFeature; -import net.minecraft.world.gen.feature.MegaJungleTreeFeature; -import net.minecraft.world.gen.feature.MegaPineTreeFeature; -import net.minecraft.world.gen.feature.OakTreeFeature; -import net.minecraft.world.gen.feature.PineTreeFeature; -import net.minecraft.world.gen.feature.PlantedFeatureConfig; -import net.minecraft.world.gen.feature.SavannaTreeFeature; -import net.minecraft.world.gen.feature.SpruceTreeFeature; -import net.minecraft.world.gen.feature.SwampTreeFeature; -import net.minecraft.world.level.LevelProperties; - -import java.io.File; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProblemReporter; +import net.minecraft.util.RandomSource; +import net.minecraft.world.Clearable; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkSource; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.status.ChunkStatus; +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.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.storage.DerivedLevelData; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import org.enginehub.linbus.tree.LinCompoundTag; + import java.lang.ref.WeakReference; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; -import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.OptionalInt; -import java.util.Random; +import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; - import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + /** * An adapter to Minecraft worlds for WorldEdit. */ public class FabricWorld extends AbstractWorld { - private static final Random random = new Random(); - private static final int UPDATE = 1, NOTIFY = 2; + private static final RandomSource random = RandomSource.create(); - private static final net.minecraft.block.BlockState JUNGLE_LOG = Blocks.JUNGLE_LOG.getDefaultState(); - private static final net.minecraft.block.BlockState JUNGLE_LEAF = Blocks.JUNGLE_LEAVES.getDefaultState().with(LeavesBlock.PERSISTENT, Boolean.TRUE); - private static final net.minecraft.block.BlockState JUNGLE_SHRUB = Blocks.OAK_LEAVES.getDefaultState().with(LeavesBlock.PERSISTENT, Boolean.TRUE); + private static ResourceLocation getDimensionRegistryKey(Level world) { + return Objects.requireNonNull(world.getServer(), "server cannot be null") + .registryAccess() + .lookupOrThrow(Registries.DIMENSION_TYPE) + .getKey(world.dimensionType()); + } - private final WeakReference worldRef; + private final WeakReference worldRef; private final FabricWorldNativeAccess worldNativeAccess; /** @@ -137,35 +158,20 @@ public class FabricWorld extends AbstractWorld { * * @param world the world */ - FabricWorld(World world) { + FabricWorld(Level world) { checkNotNull(world); this.worldRef = new WeakReference<>(world); this.worldNativeAccess = new FabricWorldNativeAccess(worldRef); } - /** - * Get the underlying handle to the world. - * - * @return the world - * @throws WorldEditException thrown if a reference to the world was lost (i.e. world was unloaded) - */ - public World getWorldChecked() throws WorldEditException { - World world = worldRef.get(); - if (world != null) { - return world; - } else { - throw new WorldReferenceLostException("The reference to the world was lost (i.e. the world may have been unloaded)"); - } - } - /** * Get the underlying handle to the world. * * @return the world * @throws RuntimeException thrown if a reference to the world was lost (i.e. world was unloaded) */ - public World getWorld() { - World world = worldRef.get(); + public Level getWorld() { + Level world = worldRef.get(); if (world != null) { return world; } else { @@ -175,32 +181,31 @@ public World getWorld() { @Override public String getName() { - return getWorld().getLevelProperties().getLevelName(); + LevelData levelProperties = getWorld().getLevelData(); + return ((ServerLevelData) levelProperties).getLevelName(); } @Override - public String getId() { - return getWorld().getLevelProperties().getLevelName() - .replace(" ", "_").toLowerCase(Locale.ROOT) - + getWorld().dimension.getType().getSuffix(); + public String id() { + return getName() + "_" + getDimensionRegistryKey(getWorld()); } @Override public Path getStoragePath() { - final World world = getWorld(); - if (world instanceof ServerWorld) { - return ((ServerWorld) world).getSaveHandler().getWorldDir().toPath(); - } - return null; + final Level world = getWorld(); + MinecraftServer server = world.getServer(); + checkState(server instanceof ExtendedMinecraftServer, "Need a server world"); + return ((ExtendedMinecraftServer) server).getStoragePath(world); } @Override public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) throws WorldEditException { + clearContainerBlockContents(position); return worldNativeAccess.setBlock(position, block, sideEffects); } @Override - public Set applySideEffects(BlockVector3 position, BlockState previousType, SideEffectSet sideEffectSet) throws WorldEditException { + public Set applySideEffects(BlockVector3 position, BlockState previousType, SideEffectSet sideEffectSet) { worldNativeAccess.applySideEffects(position, previousType, sideEffectSet); return Sets.intersection(FabricWorldEdit.inst.getPlatform().getSupportedSideEffects(), sideEffectSet.getSideEffectsToApply()); } @@ -208,69 +213,81 @@ public Set applySideEffects(BlockVector3 position, BlockState previo @Override public int getBlockLightLevel(BlockVector3 position) { checkNotNull(position); - return getWorld().getLightLevel(FabricAdapter.toBlockPos(position)); + return getWorld().getMaxLocalRawBrightness(FabricAdapter.toBlockPos(position)); } @Override public boolean clearContainerBlockContents(BlockVector3 position) { checkNotNull(position); + BlockEntity tile = getWorld().getBlockEntity(FabricAdapter.toBlockPos(position)); if ((tile instanceof Clearable)) { - ((Clearable) tile).clear(); + ((Clearable) tile).clearContent(); return true; } return false; } @Override - public BiomeType getBiome(BlockVector2 position) { + public BiomeType getBiome(BlockVector3 position) { checkNotNull(position); - return FabricAdapter.adapt(getWorld().getBiome(new BlockPos(position.getBlockX(), 0, position.getBlockZ()))); + ChunkAccess chunk = getWorld().getChunk(position.x() >> 4, position.z() >> 4); + return getBiomeInChunk(position, chunk); + } + + private BiomeType getBiomeInChunk(BlockVector3 position, ChunkAccess chunk) { + return FabricAdapter.adapt( + chunk.getNoiseBiome(position.x() >> 2, position.y() >> 2, position.z() >> 2).value() + ); } @Override - public boolean setBiome(BlockVector2 position, BiomeType biome) { + public boolean setBiome(BlockVector3 position, BiomeType biome) { checkNotNull(position); checkNotNull(biome); - Chunk chunk = getWorld().getChunk(position.getBlockX() >> 4, position.getBlockZ() >> 4, ChunkStatus.FULL, false); - if (chunk == null) { - return false; - } - chunk.getBiomeArray()[((position.getBlockZ() & 0xF) << 4 | position.getBlockX() & 0xF)] = FabricAdapter.adapt(biome); + ChunkAccess chunk = getWorld().getChunk(position.x() >> 4, position.z() >> 4); + // Screw it, we know it's really mutable... + var biomeArray = (PalettedContainer>) chunk.getSection(chunk.getSectionIndex(position.y())).getBiomes(); + biomeArray.getAndSetUnchecked( + position.x() & 3, position.y() & 3, position.z() & 3, + getWorld().registryAccess().lookup(Registries.BIOME) + .orElseThrow() + .getOrThrow(ResourceKey.create(Registries.BIOME, ResourceLocation.parse(biome.id()))) + ); + chunk.markUnsaved(); return true; } - private static final LoadingCache fakePlayers - = CacheBuilder.newBuilder().weakKeys().softValues().build(CacheLoader.from(WorldEditFakePlayer::new)); + private static final LoadingCache fakePlayers + = CacheBuilder.newBuilder().weakKeys().softValues().build(CacheLoader.from(FabricFakePlayer::new)); @Override public boolean useItem(BlockVector3 position, BaseItem item, Direction face) { - ItemStack stack = FabricAdapter.adapt(new BaseItemStack(item.getType(), item.getNbtData(), 1)); - ServerWorld world = (ServerWorld) getWorld(); - final WorldEditFakePlayer fakePlayer; + ItemStack stack = FabricAdapter.adapt(new BaseItemStack(item.getType(), item.getNbtReference(), 1)); + ServerLevel world = (ServerLevel) getWorld(); + final FabricFakePlayer fakePlayer; try { fakePlayer = fakePlayers.get(world); } catch (ExecutionException ignored) { return false; } - fakePlayer.setStackInHand(Hand.MAIN_HAND, stack); - fakePlayer.setPositionAndAngles(position.getBlockX(), position.getBlockY(), position.getBlockZ(), + fakePlayer.setItemInHand(InteractionHand.MAIN_HAND, stack); + fakePlayer.absSnapTo(position.x(), position.y(), position.z(), (float) face.toVector().toYaw(), (float) face.toVector().toPitch()); final BlockPos blockPos = FabricAdapter.toBlockPos(position); final BlockHitResult rayTraceResult = new BlockHitResult(FabricAdapter.toVec3(position), FabricAdapter.adapt(face), blockPos, false); - ItemUsageContext itemUseContext = new ItemUsageContext(fakePlayer, Hand.MAIN_HAND, rayTraceResult); - ActionResult used = stack.useOnBlock(itemUseContext); - if (used != ActionResult.SUCCESS) { + UseOnContext itemUseContext = new UseOnContext(fakePlayer, InteractionHand.MAIN_HAND, rayTraceResult); + InteractionResult used = stack.useOn(itemUseContext); + if (used != InteractionResult.SUCCESS) { // try activating the block - if (getWorld().getBlockState(blockPos).activate(world, fakePlayer, Hand.MAIN_HAND, rayTraceResult)) { - used = ActionResult.SUCCESS; - } else { - used = stack.getItem().use(world, fakePlayer, Hand.MAIN_HAND).getResult(); - } + used = getWorld().getBlockState(blockPos).useItemOn(stack, world, fakePlayer, InteractionHand.MAIN_HAND, rayTraceResult); + } + if (used != InteractionResult.SUCCESS) { + used = stack.use(world, fakePlayer, InteractionHand.MAIN_HAND); } - return used == ActionResult.SUCCESS; + return used == InteractionResult.SUCCESS; } @Override @@ -282,97 +299,266 @@ public void dropItem(Vector3 position, BaseItemStack item) { return; } - ItemEntity entity = new ItemEntity(getWorld(), position.getX(), position.getY(), position.getZ(), FabricAdapter.adapt(item)); - entity.setPickupDelay(10); - getWorld().spawnEntity(entity); + ItemEntity entity = new ItemEntity(getWorld(), position.x(), position.y(), position.z(), FabricAdapter.adapt(item)); + entity.setPickUpDelay(10); + getWorld().addFreshEntity(entity); } @Override public void simulateBlockMine(BlockVector3 position) { BlockPos pos = FabricAdapter.toBlockPos(position); - getWorld().breakBlock(pos, true); + getWorld().destroyBlock(pos, true); } @Override - public boolean regenerate(Region region, EditSession editSession) { + public boolean canPlaceAt(BlockVector3 position, BlockState blockState) { + return FabricAdapter.adapt(blockState).canSurvive(getWorld(), FabricAdapter.toBlockPos(position)); + } + + @Override + public boolean regenerate(Region region, Extent extent, RegenOptions options) { // Don't even try to regen if it's going to fail. - ChunkManager provider = getWorld().getChunkManager(); - if (!(provider instanceof ServerChunkManager)) { + ChunkSource provider = getWorld().getChunkSource(); + if (!(provider instanceof ServerChunkCache)) { return false; } - File saveFolder = Files.createTempDir(); - // register this just in case something goes wrong - // normally it should be deleted at the end of this method - saveFolder.deleteOnExit(); try { - ServerWorld originalWorld = (ServerWorld) getWorld(); - - MinecraftServer server = originalWorld.getServer(); - WorldSaveHandler saveHandler = new WorldSaveHandler(saveFolder, originalWorld.getSaveHandler().getWorldDir().getName(), server, server.getDataFixer()); - World freshWorld = new ServerWorld(server, server.getWorkerExecutor(), saveHandler, originalWorld.getLevelProperties(), - originalWorld.dimension.getType(), originalWorld.getProfiler(), new NoOpChunkStatusListener()); - - // Pre-gen all the chunks - // We need to also pull one more chunk in every direction - CuboidRegion expandedPreGen = new CuboidRegion(region.getMinimumPoint().subtract(16, 0, 16), region.getMaximumPoint().add(16, 0, 16)); - for (BlockVector2 chunk : expandedPreGen.getChunks()) { - freshWorld.getChunk(chunk.getBlockX(), chunk.getBlockZ()); - } + doRegen(region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed", e); + } + + return true; + } - FabricWorld from = new FabricWorld(freshWorld); - for (BlockVector3 vec : region) { - editSession.setBlock(vec, from.getFullBlock(vec)); + private void doRegen(Region region, Extent extent, RegenOptions options) throws Exception { + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("WorldEditTempGen")) { + ServerLevel originalWorld = (ServerLevel) getWorld(); + PrimaryLevelData levelProperties = getPrimaryLevelData(originalWorld.getLevelData()); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + levelProperties.worldOptions = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + ResourceKey worldRegKey = originalWorld.dimension(); + try (ServerLevel serverWorld = new ServerLevel( + originalWorld.getServer(), Util.backgroundExecutor(), session, + ((ServerLevelData) originalWorld.getLevelData()), + worldRegKey, + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + originalWorld.isDebug(), + seed, + // No spawners are needed for this world. + ImmutableList.of(), + // This controls ticking, we don't need it so set it to false. + false, + originalWorld.getRandomSequences() + )) { + regenForWorld(region, extent, serverWorld, options); + + // drive the server executor until all tasks are popped off + while (originalWorld.getServer().pollTask()) { + Thread.yield(); + } + } finally { + levelProperties.worldOptions = originalOpts; } - } catch (MaxChangedBlocksException e) { - throw new RuntimeException(e); } finally { - saveFolder.delete(); + SafeFiles.tryHardToDeleteDir(tempDir); } + } - return true; + private static PrimaryLevelData getPrimaryLevelData(LevelData levelData) { + if (levelData instanceof DerivedLevelData derivedLevelData) { + return getPrimaryLevelData(derivedLevelData.wrapped); + } else if (levelData instanceof PrimaryLevelData primaryLevelData) { + return primaryLevelData; + } else { + throw new IllegalStateException("Unknown level data type: " + levelData.getClass()); + } } + private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld, + RegenOptions options) throws WorldEditException { + List> chunkLoadings = submitChunkLoadTasks(region, serverWorld); + + // drive executor until loading finishes + serverWorld.getChunkSource().mainThreadProcessor + .managedBlock(() -> { + // bail out early if a future fails + if (chunkLoadings.stream().anyMatch(ftr -> + ftr.isDone() && Futures.getUnchecked(ftr) == null + )) { + return false; + } + return chunkLoadings.stream().allMatch(CompletableFuture::isDone); + }); + + Map chunks = new HashMap<>(); + for (CompletableFuture future : chunkLoadings) { + @Nullable + ChunkAccess chunk = future.getNow(null); + checkState(chunk != null, "Failed to generate a chunk, regen failed."); + chunks.put(chunk.getPos(), chunk); + } + + for (BlockVector3 vec : region) { + BlockPos pos = FabricAdapter.toBlockPos(vec); + ChunkAccess chunk = chunks.get(new ChunkPos(pos)); + BlockStateHolder state = FabricAdapter.adapt(chunk.getBlockState(pos)); + BlockEntity blockEntity = chunk.getBlockEntity(pos); + if (blockEntity != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, getWorld().registryAccess()); + blockEntity.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + state = state.toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(tag))); + } + extent.setBlock(vec, state.toBaseBlock()); + + if (options.shouldRegenBiomes()) { + BiomeType biome = getBiomeInChunk(vec, chunk); + extent.setBiome(vec, biome); + } + } + } + + private List> submitChunkLoadTasks(Region region, ServerLevel world) { + List> chunkLoadings = new ArrayList<>(); + // Pre-gen all the chunks + for (BlockVector2 chunk : region.getChunks()) { + chunkLoadings.add( + world.getChunkSource().getChunkFuture(chunk.x(), chunk.z(), ChunkStatus.FEATURES, true) + .thenApply(either -> either.orElse(null)) + ); + } + return chunkLoadings; + } + + @SuppressWarnings("deprecation") @Nullable - private static Feature createTreeFeatureGenerator(TreeType type) { - switch (type) { - case TREE: return new OakTreeFeature(DefaultFeatureConfig::deserialize, true); - case BIG_TREE: return new LargeOakTreeFeature(DefaultFeatureConfig::deserialize, true); - case REDWOOD: return new PineTreeFeature(DefaultFeatureConfig::deserialize); - case TALL_REDWOOD: return new SpruceTreeFeature(DefaultFeatureConfig::deserialize, true); - case BIRCH: return new BirchTreeFeature(DefaultFeatureConfig::deserialize, true, false); - case JUNGLE: return new MegaJungleTreeFeature(DefaultFeatureConfig::deserialize, true, 10, 20, JUNGLE_LOG, JUNGLE_LEAF); - case SMALL_JUNGLE: return new JungleTreeFeature(DefaultFeatureConfig::deserialize, true, 4 + random.nextInt(7), JUNGLE_LOG, JUNGLE_LEAF, false); - case SHORT_JUNGLE: return new JungleTreeFeature(DefaultFeatureConfig::deserialize, true, 4 + random.nextInt(7), JUNGLE_LOG, JUNGLE_LEAF, true); - case JUNGLE_BUSH: return new JungleGroundBushFeature(DefaultFeatureConfig::deserialize, JUNGLE_LOG, JUNGLE_SHRUB); - case SWAMP: return new SwampTreeFeature(DefaultFeatureConfig::deserialize); - case ACACIA: return new SavannaTreeFeature(DefaultFeatureConfig::deserialize, true); - case DARK_OAK: return new DarkOakTreeFeature(DefaultFeatureConfig::deserialize, true); - case MEGA_REDWOOD: return new MegaPineTreeFeature(DefaultFeatureConfig::deserialize, true, random.nextBoolean()); - case TALL_BIRCH: return new BirchTreeFeature(DefaultFeatureConfig::deserialize, true, true); - case RED_MUSHROOM: return new HugeRedMushroomFeature(PlantedFeatureConfig::deserialize); - case BROWN_MUSHROOM: return new HugeBrownMushroomFeature(PlantedFeatureConfig::deserialize); - case RANDOM: return createTreeFeatureGenerator(TreeType.values()[ThreadLocalRandom.current().nextInt(TreeType.values().length)]); - default: - return null; - } - } - - private FeatureConfig createFeatureConfig(TreeType type) { - if (type == TreeType.RED_MUSHROOM || type == TreeType.BROWN_MUSHROOM) { - return new PlantedFeatureConfig(true); - } else { - return new DefaultFeatureConfig(); + private static ResourceKey createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType type) { + return switch (type) { + // Based off of the SaplingGenerator class, as well as uses of DefaultBiomeFeatures fields + case TREE -> TreePlacements.OAK_CHECKED; + case BIG_TREE -> TreePlacements.FANCY_OAK_CHECKED; + case REDWOOD -> TreePlacements.SPRUCE_CHECKED; + case TALL_REDWOOD -> TreePlacements.MEGA_SPRUCE_CHECKED; + case MEGA_REDWOOD -> TreePlacements.MEGA_PINE_CHECKED; + case BIRCH -> TreePlacements.BIRCH_CHECKED; + case JUNGLE -> TreePlacements.MEGA_JUNGLE_TREE_CHECKED; + case SMALL_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case SHORT_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case JUNGLE_BUSH -> TreePlacements.JUNGLE_BUSH; + case SWAMP -> TreePlacements.OAK_CHECKED; + case ACACIA -> TreePlacements.ACACIA_CHECKED; + case DARK_OAK -> TreePlacements.DARK_OAK_CHECKED; + case TALL_BIRCH -> TreePlacements.SUPER_BIRCH_BEES_0002; + case WARPED_FUNGUS -> TreePlacements.WARPED_FUNGI; + case CRIMSON_FUNGUS -> TreePlacements.CRIMSON_FUNGI; + case CHORUS_PLANT -> EndPlacements.CHORUS_PLANT; + case MANGROVE -> TreePlacements.MANGROVE_CHECKED; + case TALL_MANGROVE -> TreePlacements.TALL_MANGROVE_CHECKED; + case CHERRY -> TreePlacements.CHERRY_CHECKED; + case PALE_OAK -> TreePlacements.PALE_OAK_CHECKED; + case PALE_OAK_CREAKING -> TreePlacements.PALE_OAK_CREAKING_CHECKED; + case RANDOM -> createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType.values()[ThreadLocalRandom.current().nextInt(com.sk89q.worldedit.util.TreeGenerator.TreeType.values().length)]); + default -> null; + }; + } + + @SuppressWarnings("deprecation") + @Override + public boolean generateTree(com.sk89q.worldedit.util.TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { + ServerLevel world = (ServerLevel) getWorld(); + PlacedFeature generator = Optional.ofNullable(createTreeFeatureGenerator(type)) + .map(k -> world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(k)) + .orElse(null); + ServerChunkCache chunkManager = world.getChunkSource(); + if (type == com.sk89q.worldedit.util.TreeGenerator.TreeType.CHORUS_PLANT) { + position = position.add(0, 1, 0); + } + try (FabricServerLevelDelegateProxy.LevelAndProxy proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + FabricAdapter.toBlockPos(position) + ); } } @Override public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { - @SuppressWarnings("unchecked") - Feature generator = (Feature) createTreeFeatureGenerator(type); - return generator != null - && generator.generate(getWorld(), getWorld().getChunkManager().getChunkGenerator(), random, - FabricAdapter.toBlockPos(position), createFeatureConfig(type)); + ServerLevel world = (ServerLevel) getWorld(); + PlacedFeature generator = world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ServerChunkCache chunkManager = world.getChunkSource(); + try (FabricServerLevelDelegateProxy.LevelAndProxy proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + FabricAdapter.toBlockPos(position) + ); + } + } + + @Override + public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = (ServerLevel) getWorld(); + ConfiguredFeature feature = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ServerChunkCache chunkManager = world.getChunkSource(); + try (FabricServerLevelDelegateProxy.LevelAndProxy proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world)) { + return feature != null && feature.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + FabricAdapter.toBlockPos(position) + ); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean generateStructure(StructureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = (ServerLevel) getWorld(); + Registry structureRegistry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Structure structure = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); + if (structure == null) { + return false; + } + + ServerChunkCache chunkManager = world.getChunkSource(); + try (FabricServerLevelDelegateProxy.LevelAndProxy proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world)) { + ChunkPos chunkPos = new ChunkPos(new BlockPos(position.x(), position.y(), position.z())); + StructureStart structureStart = structure.generate( + structureRegistry.wrapAsHolder(structure), world.dimension(), world.registryAccess(), + chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), + world.getStructureManager(), world.getSeed(), chunkPos, 0, proxyLevel.level(), + biome -> true + ); + + if (!structureStart.isValid()) { + return false; + } else { + BoundingBox boundingBox = structureStart.getBoundingBox(); + ChunkPos min = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); + ChunkPos max = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); + ChunkPos.rangeClosed(min, max).forEach((chunkPosx) -> + structureStart.placeInChunk( + proxyLevel.level(), world.structureManager(), chunkManager.getGenerator(), world.getRandom(), + new BoundingBox(chunkPosx.getMinBlockX(), world.getMinY(), chunkPosx.getMinBlockZ(), + chunkPosx.getMaxBlockX(), world.getMaxY(), chunkPosx.getMaxBlockZ()), + chunkPosx + ) + ); + return true; + } + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } } @Override @@ -386,22 +572,27 @@ public void fixAfterFastMode(Iterable chunks) { } @Override - public void fixLighting(Iterable chunks) { - World world = getWorld(); + public void sendBiomeUpdates(Iterable chunks) { + List nativeChunks = chunks instanceof Collection chunkCollection ? Lists.newArrayListWithCapacity(chunkCollection.size()) : Lists.newArrayList(); for (BlockVector2 chunk : chunks) { - world.getChunkManager().getLightingProvider().suppressLight(new ChunkPos(chunk.getBlockX(), chunk.getBlockZ()), true); + nativeChunks.add(getWorld().getChunk(chunk.x(), chunk.z(), ChunkStatus.BIOMES, false)); } + ((ServerLevel) getWorld()).getChunkSource().chunkMap.resendBiomesForChunks(nativeChunks); } @Override - public boolean playEffect(Vector3 position, int type, int data) { - getWorld().playLevelEvent(type, FabricAdapter.toBlockPos(position.toBlockPoint()), data); - return true; + public void fixLighting(Iterable chunks) { + Level world = getWorld(); + for (BlockVector2 chunk : chunks) { + world.getChunkSource().getLightEngine().setLightEnabled( + new ChunkPos(chunk.x(), chunk.z()), true + ); + } } @Override public WeatherType getWeather() { - LevelProperties info = getWorld().getLevelProperties(); + LevelData info = getWorld().getLevelData(); if (info.isThundering()) { return WeatherTypes.THUNDER_STORM; } @@ -413,7 +604,7 @@ public WeatherType getWeather() { @Override public long getRemainingWeatherDuration() { - LevelProperties info = getWorld().getLevelProperties(); + ServerLevelData info = (ServerLevelData) getWorld().getLevelData(); if (info.isThundering()) { return info.getThunderTime(); } @@ -430,7 +621,7 @@ public void setWeather(WeatherType weatherType) { @Override public void setWeather(WeatherType weatherType, long duration) { - LevelProperties info = getWorld().getLevelProperties(); + ServerLevelData info = (ServerLevelData) getWorld().getLevelData(); if (weatherType == WeatherTypes.THUNDER_STORM) { info.setClearWeatherTime(0); info.setThundering(true); @@ -446,40 +637,41 @@ public void setWeather(WeatherType weatherType, long duration) { } } + @Override + public int getMinY() { + return getWorld().getMinY(); + } + @Override public int getMaxY() { - return getWorld().getHeight() - 1; + return getWorld().getMaxY(); } @Override public BlockVector3 getSpawnPosition() { - return FabricAdapter.adapt(getWorld().getSpawnPos()); + return FabricAdapter.adapt(getWorld().getLevelData().getRespawnData().pos()); } @Override public BlockState getBlock(BlockVector3 position) { - net.minecraft.block.BlockState mcState = getWorld() - .getChunk(position.getBlockX() >> 4, position.getBlockZ() >> 4) + net.minecraft.world.level.block.state.BlockState mcState = getWorld() + .getChunk(position.x() >> 4, position.z() >> 4) .getBlockState(FabricAdapter.toBlockPos(position)); - BlockState matchingBlock = BlockStateIdAccess.getBlockStateById(Block.getRawIdFromState(mcState)); - if (matchingBlock != null) { - return matchingBlock; - } - return FabricAdapter.adapt(mcState); } @Override public BaseBlock getFullBlock(BlockVector3 position) { - BlockPos pos = new BlockPos(position.getBlockX(), position.getBlockY(), position.getBlockZ()); + BlockPos pos = new BlockPos(position.x(), position.y(), position.z()); // Avoid creation by using the CHECK mode -- if it's needed, it'll be re-created anyways - BlockEntity tile = ((WorldChunk) getWorld().getChunk(pos)).getBlockEntity(pos, WorldChunk.CreationType.CHECK); + BlockEntity tile = ((LevelChunk) getWorld().getChunk(pos)).getBlockEntity(pos, LevelChunk.EntityCreationType.CHECK); if (tile != null) { - net.minecraft.nbt.CompoundTag tag = new net.minecraft.nbt.CompoundTag(); - tile.toTag(tag); - return getBlock(position).toBaseBlock(NBTConverter.fromNative(tag)); + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, getWorld().registryAccess()); + tile.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + return getBlock(position).toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(tag))); } else { return getBlock(position).toBaseBlock(); } @@ -492,10 +684,9 @@ public int hashCode() { @Override public boolean equals(Object o) { - if ((o instanceof FabricWorld)) { - FabricWorld other = ((FabricWorld) o); - World otherWorld = other.worldRef.get(); - World thisWorld = worldRef.get(); + if ((o instanceof FabricWorld other)) { + Level otherWorld = other.worldRef.get(); + Level thisWorld = worldRef.get(); return otherWorld != null && otherWorld.equals(thisWorld); } else if (o instanceof com.sk89q.worldedit.world.World) { return ((com.sk89q.worldedit.world.World) o).getName().equals(getName()); @@ -506,75 +697,87 @@ public boolean equals(Object o) { @Override public List getEntities(Region region) { - final World world = getWorld(); - if (!(world instanceof ServerWorld)) { - return Collections.emptyList(); - } - return ((ServerWorld) world).getEntities(null, entity -> true) - .stream() - .filter(e -> region.contains(FabricAdapter.adapt(e.getBlockPos()))) - .map(FabricEntity::new).collect(Collectors.toList()); + final Level world = getWorld(); + AABB box = new AABB( + FabricAdapter.toVec3(region.getMinimumPoint()), + FabricAdapter.toVec3(region.getMaximumPoint().add(BlockVector3.ONE)) + ); + List nmsEntities = world.getEntities( + (net.minecraft.world.entity.Entity) null, + box, + e -> region.contains(FabricAdapter.adapt(e.blockPosition())) + ); + return nmsEntities.stream() + .map(FabricEntity::new) + .collect(ImmutableList.toImmutableList()); } @Override public List getEntities() { - final World world = getWorld(); - if (!(world instanceof ServerWorld)) { + final Level world = getWorld(); + if (!(world instanceof ServerLevel)) { return Collections.emptyList(); } - return ((ServerWorld) world).getEntities(null, entity -> true) - .stream() - .map(FabricEntity::new) - .collect(Collectors.toList()); + return Streams.stream(((ServerLevel) world).getAllEntities()) + .map(FabricEntity::new) + .collect(ImmutableList.toImmutableList()); } @Nullable @Override public Entity createEntity(Location location, BaseEntity entity) { - World world = getWorld(); - final Optional> entityType = EntityType.get(entity.getType().getId()); - if (!entityType.isPresent()) return null; - net.minecraft.entity.Entity createdEntity = entityType.get().create(world); - if (createdEntity != null) { - CompoundTag nativeTag = entity.getNbtData(); - if (nativeTag != null) { - net.minecraft.nbt.CompoundTag tag = NBTConverter.toNative(entity.getNbtData()); - for (String name : Constants.NO_COPY_ENTITY_NBT_FIELDS) { - tag.remove(name); - } - createdEntity.fromTag(tag); - } - - createdEntity.setPositionAndAngles(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); - - world.spawnEntity(createdEntity); - return new FabricEntity(createdEntity); - } else { + ServerLevel world = (ServerLevel) getWorld(); + String entityId = entity.getType().id(); + final Optional> entityType = EntityType.byString(entityId); + if (entityType.isEmpty()) { return null; } - } + LinCompoundTag linTag = entity.getNbt(); + net.minecraft.nbt.CompoundTag tag; + if (linTag != null) { + tag = NBTConverter.toNative(linTag); + removeUnwantedEntityTagsRecursively(tag); + } else { + tag = new net.minecraft.nbt.CompoundTag(); + } + tag.putString("id", entityId); - /** - * Thrown when the reference to the world is lost. - */ - @SuppressWarnings("serial") - private static final class WorldReferenceLostException extends WorldEditException { - private WorldReferenceLostException(String message) { - super(message); + net.minecraft.world.entity.Entity createdEntity = EntityType.loadEntityRecursive(tag, world, EntitySpawnReason.COMMAND, (loadedEntity) -> { + loadedEntity.absSnapTo(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + return loadedEntity; + }); + if (createdEntity != null) { + world.addFreshEntityWithPassengers(createdEntity); + return new FabricEntity(createdEntity); } + return null; } - private static class NoOpChunkStatusListener implements WorldGenerationProgressListener { - @Override - public void start(ChunkPos chunkPos) { + private void removeUnwantedEntityTagsRecursively(net.minecraft.nbt.CompoundTag tag) { + for (String name : Constants.NO_COPY_ENTITY_NBT_FIELDS) { + tag.remove(name); } - @Override - public void setChunkStatus(ChunkPos chunkPos, @Nullable ChunkStatus chunkStatus) { - } + // Adapted from net.minecraft.world.entity.EntityType#loadEntityRecursive + tag.getList("Passengers").ifPresent(nbttaglist -> { + for (int i = 0; i < nbttaglist.size(); ++i) { + removeUnwantedEntityTagsRecursively(nbttaglist.getCompoundOrEmpty(i)); + } + }); + } - @Override - public void stop() { - } + @Override + public Mask createLiquidMask() { + return new AbstractExtentMask(this) { + @Override + public boolean test(BlockVector3 vector) { + return FabricAdapter.adapt(getExtent().getBlock(vector)).getBlock() instanceof LiquidBlock; + } + }; + } + + @Override + public boolean isValid() { + return worldRef.get() != null; } } 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 755ee118e2..29a1021898 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 @@ -22,22 +22,34 @@ import com.mojang.brigadier.CommandDispatcher; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.command.util.PermissionCondition; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; +import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; +import com.sk89q.worldedit.event.platform.PlatformsRegisteredEvent; import com.sk89q.worldedit.event.platform.SessionIdleEvent; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extension.platform.PlatformManager; -import com.sk89q.worldedit.fabric.net.handler.WECUIPacketHandler; import com.sk89q.worldedit.internal.anvil.ChunkDeleter; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; +import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.lifecycle.Lifecycled; +import com.sk89q.worldedit.util.lifecycle.SimpleLifecycled; +import com.sk89q.worldedit.world.biome.BiomeCategory; import com.sk89q.worldedit.world.biome.BiomeType; 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.gamemode.GameModes; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; +import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; +import com.sk89q.worldedit.world.weather.WeatherTypes; import net.fabricmc.api.ModInitializer; -import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.player.AttackBlockCallback; @@ -46,30 +58,48 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.ItemStack; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.metadata.ModMetadata; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.network.ServerPlayNetworkHandler; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.tag.BlockTags; -import net.minecraft.tag.ItemTags; -import net.minecraft.util.ActionResult; -import net.minecraft.util.Hand; -import net.minecraft.util.Identifier; -import net.minecraft.util.TypedActionResult; -import net.minecraft.util.hit.BlockHitResult; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Direction; -import net.minecraft.util.registry.Registry; -import net.minecraft.world.World; -import org.apache.logging.log4j.LogManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.phys.BlockHitResult; import org.apache.logging.log4j.Logger; +import org.enginehub.piston.Command; +import org.enginehub.worldeditcui.protocol.CUIPacket; +import org.enginehub.worldeditcui.protocol.CUIPacketHandler; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; import static com.sk89q.worldedit.fabric.FabricAdapter.adaptPlayer; @@ -80,14 +110,39 @@ */ public class FabricWorldEdit implements ModInitializer { - private static final Logger LOGGER = LogManager.getLogger(); + private static final Logger LOGGER = LogManagerCompat.getLogger(); public static final String MOD_ID = "worldedit"; - public static final String CUI_PLUGIN_CHANNEL = "cui"; + + public static final Lifecycled LIFECYCLED_SERVER; + + static { + SimpleLifecycled lifecycledServer = SimpleLifecycled.invalid(); + ServerLifecycleEvents.SERVER_STARTED.register(lifecycledServer::newValue); + ServerLifecycleEvents.SERVER_STOPPING.register(__ -> lifecycledServer.invalidate()); + LIFECYCLED_SERVER = lifecycledServer; + } + + /** + * {@return current server's registry access} Not for long-term storage. + */ + public static RegistryAccess registryAccess() { + return LIFECYCLED_SERVER.valueOrThrow().registryAccess(); + } + + /** + * {@return current server's registry} Not for long-term storage. + * + * @param key the registry key + */ + public static Registry getRegistry(ResourceKey> key) { + return LIFECYCLED_SERVER.valueOrThrow().registryAccess().lookupOrThrow(key); + } private FabricPermissionsProvider provider; public static FabricWorldEdit inst; + private InteractionDebouncer debouncer; private FabricPlatform platform; private FabricConfiguration config; private Path workingDir; @@ -113,8 +168,15 @@ public void onInitialize() { throw new UncheckedIOException(e); } } + this.platform = new FabricPlatform(this); + debouncer = new InteractionDebouncer(platform); - WECUIPacketHandler.init(); + WorldEdit.getInstance().getPlatformManager().register(platform); + + config = new FabricConfiguration(this); + this.provider = getInitialPermissionsProvider(); + + CUIPacketHandler.instance().registerServerboundHandler(this::onCuiPacket); ServerTickEvents.END_SERVER_TICK.register(ThreadSafeCache.getInstance()); CommandRegistrationCallback.EVENT.register(this::registerCommands); @@ -124,83 +186,143 @@ public void onInitialize() { ServerPlayConnectionEvents.DISCONNECT.register(this::onPlayerDisconnect); AttackBlockCallback.EVENT.register(this::onLeftClickBlock); UseBlockCallback.EVENT.register(this::onRightClickBlock); - UseItemCallback.EVENT.register(this::onRightClickAir); + UseItemCallback.EVENT.register(this::onRightClickItem); LOGGER.info("WorldEdit for Fabric (version " + getInternalVersion() + ") is loaded"); } - private void registerCommands(CommandDispatcher dispatcher, boolean dedicated) { + private void registerCommands(CommandDispatcher dispatcher, CommandBuildContext registryAccess, Commands.CommandSelection environment) { + WorldEdit.getInstance().getEventBus().post(new PlatformsRegisteredEvent()); PlatformManager manager = WorldEdit.getInstance().getPlatformManager(); - if (manager.getPlatforms().isEmpty()) { - // We'll register as part of our platform initialization later. - return; - } - - // This is a re-register (due to /reload), we must add our commands now - Platform commandsPlatform = manager.queryCapability(Capability.USER_COMMANDS); if (commandsPlatform != platform || !platform.isHookingEvents()) { - // We're not in control of commands/events -- do not re-register. + // We're not in control of commands/events -- do not register. return; } - platform.setNativeDispatcher(dispatcher); - platform.registerCommands(manager.getPlatformCommandManager().getCommandManager()); - } - private void setupPlatform(MinecraftServer server) { - this.platform = new FabricPlatform(this, server); - - WorldEdit.getInstance().getPlatformManager().register(platform); - - this.provider = getInitialPermissionsProvider(); + List commands = manager.getPlatformCommandManager().getCommandManager() + .getAllCommands().toList(); + for (Command command : commands) { + CommandWrapper.register(dispatcher, command); + Set perms = command.getCondition().as(PermissionCondition.class) + .map(PermissionCondition::getPermissions) + .orElseGet(Collections::emptySet); + if (!perms.isEmpty()) { + perms.forEach(getPermissionsProvider()::registerPermission); + } + } } private FabricPermissionsProvider getInitialPermissionsProvider() { try { Class.forName("me.lucko.fabric.api.permissions.v0.Permissions", false, getClass().getClassLoader()); + Optional version = FabricLoader.getInstance().getModContainer("fabric-permissions-api-v0") + .map(ModContainer::getMetadata) + .map(ModMetadata::getVersion); + + if (version.isPresent() && !VersionPredicate.parse(">=0.5.0").test(version.get())) { + throw new RuntimeException("Fabric permissions version " + version.get() + " is not supported. Please update Fabric Permissions API"); + } + return new FabricPermissionsProvider.LuckoFabricPermissionsProvider(platform); } catch (ClassNotFoundException ignored) { // fallback to vanilla + } catch (Exception e) { + // catch any exception to prevent crashing the server, but still print a warning + LOGGER.warn("Failed to load Fabric permissions provider. Falling back to Minecraft", e); } + return new FabricPermissionsProvider.VanillaPermissionsProvider(platform); } private void setupRegistries(MinecraftServer server) { // Blocks - for (Identifier name : Registry.BLOCK.getIds()) { - if (BlockType.REGISTRY.get(name.toString()) == null) { - BlockType.REGISTRY.register(name.toString(), new BlockType(name.toString(), - input -> FabricAdapter.adapt(FabricAdapter.adapt(input.getBlockType()).getDefaultState()))); + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.BLOCK).keySet()) { + String key = name.toString(); + if (BlockType.REGISTRY.get(key) == null) { + BlockType.REGISTRY.register(key, new BlockType(key, + input -> FabricAdapter.adapt(FabricAdapter.adapt(input.getBlockType()).defaultBlockState()))); } } // Items - for (Identifier name : Registry.ITEM.getIds()) { - if (ItemType.REGISTRY.get(name.toString()) == null) { - ItemType.REGISTRY.register(name.toString(), new ItemType(name.toString())); + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.ITEM).keySet()) { + String key = name.toString(); + if (ItemType.REGISTRY.get(key) == null) { + ItemType.REGISTRY.register(key, new ItemType(key)); } } // Entities - for (Identifier name : Registry.ENTITY_TYPE.getIds()) { - if (EntityType.REGISTRY.get(name.toString()) == null) { - EntityType.REGISTRY.register(name.toString(), new EntityType(name.toString())); + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.ENTITY_TYPE).keySet()) { + String key = name.toString(); + if (EntityType.REGISTRY.get(key) == null) { + EntityType.REGISTRY.register(key, new EntityType(key)); } } // Biomes - for (Identifier name : server.getRegistryManager().get(Registry.BIOME_KEY).getIds()) { - if (BiomeType.REGISTRY.get(name.toString()) == null) { - BiomeType.REGISTRY.register(name.toString(), new BiomeType(name.toString())); + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.BIOME).keySet()) { + String key = name.toString(); + if (BiomeType.REGISTRY.get(key) == null) { + BiomeType.REGISTRY.register(key, new BiomeType(key)); } } // Tags - for (Identifier name : BlockTags.getTagGroup().getTagIds()) { - if (BlockCategory.REGISTRY.get(name.toString()) == null) { - BlockCategory.REGISTRY.register(name.toString(), new BlockCategory(name.toString())); + server.registryAccess().lookupOrThrow(Registries.BLOCK).getTags().map(t -> t.key().location()).forEach(name -> { + String key = name.toString(); + if (BlockCategory.REGISTRY.get(key) == null) { + BlockCategory.REGISTRY.register(key, new BlockCategory(key)); + } + }); + server.registryAccess().lookupOrThrow(Registries.ITEM).getTags().map(t -> t.key().location()).forEach(name -> { + String key = name.toString(); + if (ItemCategory.REGISTRY.get(key) == null) { + ItemCategory.REGISTRY.register(key, new ItemCategory(key)); + } + }); + Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); + biomeRegistry.getTags().forEach(tag -> { + String key = tag.key().location().toString(); + if (BiomeCategory.REGISTRY.get(key) == null) { + BiomeCategory.REGISTRY.register(key, new BiomeCategory( + key, + () -> biomeRegistry.get(tag.key()) + .stream() + .flatMap(HolderSet.Named::stream) + .map(Holder::value) + .map(FabricAdapter::adapt) + .collect(Collectors.toSet())) + ); + } + }); + // Features + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).keySet()) { + String key = name.toString(); + if (ConfiguredFeatureType.REGISTRY.get(key) == null) { + ConfiguredFeatureType.REGISTRY.register(key, new ConfiguredFeatureType(key)); + } + } + // Structures + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.STRUCTURE).keySet()) { + String key = name.toString(); + if (StructureType.REGISTRY.get(key) == null) { + StructureType.REGISTRY.register(key, new StructureType(key)); } } - for (Identifier name : ItemTags.getTagGroup().getTagIds()) { - if (ItemCategory.REGISTRY.get(name.toString()) == null) { - ItemCategory.REGISTRY.register(name.toString(), new ItemCategory(name.toString())); + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } } } + + // ... :| + GameModes.get(""); + WeatherTypes.get(""); + com.sk89q.worldedit.registry.Registries.get(""); } private void onStartingServer(MinecraftServer minecraftServer) { @@ -211,105 +333,120 @@ private void onStartingServer(MinecraftServer minecraftServer) { } private void onStartServer(MinecraftServer minecraftServer) { - FabricAdapter.setServer(minecraftServer); - setupPlatform(minecraftServer); setupRegistries(minecraftServer); - config = new FabricConfiguration(this); config.load(); - WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent()); - minecraftServer.reloadResources( - minecraftServer.getDataPackManager().getEnabledNames() - ); + WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform)); } private void onStopServer(MinecraftServer minecraftServer) { WorldEdit worldEdit = WorldEdit.getInstance(); worldEdit.getSessionManager().unload(); - worldEdit.getPlatformManager().unregister(platform); + WorldEdit.getInstance().getEventBus().post(new PlatformUnreadyEvent(platform)); } - private boolean shouldSkip() { - if (platform == null) { - return true; - } + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } - return !platform.isHookingEvents(); // We have to be told to catch these events + private boolean skipInteractionEvent(Player player, InteractionHand hand) { + return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide() || !(player instanceof ServerPlayer); } - private ActionResult onLeftClickBlock(PlayerEntity playerEntity, World world, Hand hand, BlockPos blockPos, Direction direction) { - if (shouldSkip() || hand == Hand.OFF_HAND || world.isClient) { - return ActionResult.PASS; + private InteractionResult onLeftClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockPos blockPos, Direction direction) { + if (skipInteractionEvent(playerEntity, hand)) { + return InteractionResult.PASS; } WorldEdit we = WorldEdit.getInstance(); - FabricPlayer player = adaptPlayer((ServerPlayerEntity) playerEntity); + FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); FabricWorld localWorld = getWorld(world); Location pos = new Location(localWorld, - blockPos.getX(), - blockPos.getY(), - blockPos.getZ() + blockPos.getX(), + blockPos.getY(), + blockPos.getZ() ); com.sk89q.worldedit.util.Direction weDirection = FabricAdapter.adaptEnumFacing(direction); - if (we.handleBlockLeftClick(player, pos, weDirection)) { - return ActionResult.SUCCESS; - } + boolean result = we.handleBlockLeftClick(player, pos, weDirection) || we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); - if (we.handleArmSwing(player)) { - return ActionResult.SUCCESS; - } - - return ActionResult.PASS; + return result ? InteractionResult.SUCCESS : InteractionResult.PASS; } - private ActionResult onRightClickBlock(PlayerEntity playerEntity, World world, Hand hand, BlockHitResult blockHitResult) { - if (shouldSkip() || hand == Hand.OFF_HAND || world.isClient) { - return ActionResult.PASS; + private InteractionResult onRightClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockHitResult blockHitResult) { + if (skipInteractionEvent(playerEntity, hand)) { + return InteractionResult.PASS; } WorldEdit we = WorldEdit.getInstance(); - FabricPlayer player = adaptPlayer((ServerPlayerEntity) playerEntity); + FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); FabricWorld localWorld = getWorld(world); Location pos = new Location(localWorld, - blockHitResult.getBlockPos().getX(), - blockHitResult.getBlockPos().getY(), - blockHitResult.getBlockPos().getZ() + blockHitResult.getBlockPos().getX(), + blockHitResult.getBlockPos().getY(), + blockHitResult.getBlockPos().getZ() ); - com.sk89q.worldedit.util.Direction direction = FabricAdapter.adaptEnumFacing(blockHitResult.getSide()); + com.sk89q.worldedit.util.Direction direction = FabricAdapter.adaptEnumFacing(blockHitResult.getDirection()); + + boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + return result ? InteractionResult.SUCCESS : InteractionResult.PASS; + } - if (we.handleBlockRightClick(player, pos, direction)) { - return ActionResult.SUCCESS; + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand)) { + return; } - if (we.handleRightClick(player)) { - return ActionResult.SUCCESS; + WorldEdit we = WorldEdit.getInstance(); + FabricPlayer player = adaptPlayer(playerEntity); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return; } - return ActionResult.PASS; + boolean result = we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); } - private TypedActionResult onRightClickAir(PlayerEntity playerEntity, World world, Hand hand) { - ItemStack stackInHand = playerEntity.getStackInHand(hand); - if (shouldSkip() || hand == Hand.OFF_HAND || world.isClient) { - return TypedActionResult.pass(stackInHand); + private InteractionResult onRightClickItem(Player playerEntity, Level world, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand)) { + return InteractionResult.PASS; } WorldEdit we = WorldEdit.getInstance(); - FabricPlayer player = adaptPlayer((ServerPlayerEntity) playerEntity); + FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); - if (we.handleRightClick(player)) { - return TypedActionResult.success(stackInHand); + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return previousResult.get() ? InteractionResult.SUCCESS : InteractionResult.PASS; } - return TypedActionResult.pass(stackInHand); + boolean result = we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + return result ? InteractionResult.SUCCESS : InteractionResult.PASS; } - // TODO Pass empty left click to server + private void onPlayerDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) { + debouncer.clearInteraction(adaptPlayer(handler.player)); - private void onPlayerDisconnect(ServerPlayNetworkHandler handler, MinecraftServer server) { WorldEdit.getInstance().getEventBus() - .post(new SessionIdleEvent(new FabricPlayer.SessionKeyImpl(handler.player))); + .post(new SessionIdleEvent(new FabricPlayer.SessionKeyImpl(handler.player))); + } + + private void onCuiPacket(CUIPacket payload, CUIPacketHandler.PacketContext context) { + if (!(context.player() instanceof ServerPlayer player)) { + // Ignore - this is not a server-bound packet + return; + } + + FabricPlayer actor = FabricAdapter.adaptPlayer(player); + LocalSession session = WorldEdit.getInstance().getSessionManager().get(actor); + session.handleCUIInitializationMessage(payload.eventType(), payload.args(), actor); } /** @@ -327,7 +464,7 @@ FabricConfiguration getConfig() { * @param player the player * @return the session */ - public LocalSession getSession(ServerPlayerEntity player) { + public LocalSession getSession(ServerPlayer player) { checkNotNull(player); return WorldEdit.getInstance().getSessionManager().get(adaptPlayer(player)); } @@ -338,7 +475,7 @@ public LocalSession getSession(ServerPlayerEntity player) { * @param world the world * @return the WorldEdit world */ - public FabricWorld getWorld(World world) { + public FabricWorld getWorld(Level world) { checkNotNull(world); return new FabricWorld(world); } diff --git a/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorld.java b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorld.java new file mode 100644 index 0000000000..632352a53d --- /dev/null +++ b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorld.java @@ -0,0 +1,765 @@ +/* + * 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.neoforge; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.Futures; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.blocks.BaseItem; +import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.function.mask.AbstractExtentMask; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.internal.Constants; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.neoforge.internal.NBTConverter; +import com.sk89q.worldedit.neoforge.internal.NeoForgeEntity; +import com.sk89q.worldedit.neoforge.internal.NeoForgeServerLevelDelegateProxy; +import com.sk89q.worldedit.neoforge.internal.NeoForgeWorldNativeAccess; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.Direction; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.SideEffect; +import com.sk89q.worldedit.util.SideEffectSet; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.util.io.file.SafeFiles; +import com.sk89q.worldedit.world.AbstractWorld; +import com.sk89q.worldedit.world.RegenOptions; +import com.sk89q.worldedit.world.biome.BiomeType; +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.generation.StructureType; +import com.sk89q.worldedit.world.item.ItemTypes; +import com.sk89q.worldedit.world.weather.WeatherType; +import com.sk89q.worldedit.world.weather.WeatherTypes; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.placement.EndPlacements; +import net.minecraft.data.worldgen.placement.TreePlacements; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProblemReporter; +import net.minecraft.util.RandomSource; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.Clearable; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.status.ChunkStatus; +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.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import org.enginehub.linbus.tree.LinCompoundTag; + +import java.lang.ref.WeakReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * An adapter to Minecraft worlds for WorldEdit. + */ +public class NeoForgeWorld extends AbstractWorld { + + private static final RandomSource random = RandomSource.create(); + + private static ResourceLocation getDimensionRegistryKey(ServerLevel world) { + return Objects.requireNonNull(world.getServer(), "server cannot be null") + .registryAccess() + .lookupOrThrow(Registries.DIMENSION_TYPE) + .getKey(world.dimensionType()); + } + + private final WeakReference worldRef; + private final NeoForgeWorldNativeAccess nativeAccess; + + /** + * Construct a new world. + * + * @param world the world + */ + NeoForgeWorld(ServerLevel world) { + checkNotNull(world); + this.worldRef = new WeakReference<>(world); + this.nativeAccess = new NeoForgeWorldNativeAccess(worldRef); + } + + /** + * Get the underlying handle to the world. + * + * @return the world + * @throws RuntimeException thrown if a reference to the world was lost (i.e. world was unloaded) + */ + public ServerLevel getWorld() { + ServerLevel world = worldRef.get(); + if (world != null) { + return world; + } else { + throw new RuntimeException("The reference to the world was lost (i.e. the world may have been unloaded)"); + } + } + + @Override + public String getName() { + return ((ServerLevelData) getWorld().getLevelData()).getLevelName(); + } + + @Override + public String id() { + return getName() + "_" + getDimensionRegistryKey(getWorld()); + } + + @Override + public Path getStoragePath() { + final ServerLevel world = getWorld(); + return world.getServer().storageSource.getDimensionPath(world.dimension()); + } + + @Override + public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) throws WorldEditException { + clearContainerBlockContents(position); + return nativeAccess.setBlock(position, block, sideEffects); + } + + @Override + public Set applySideEffects(BlockVector3 position, BlockState previousType, SideEffectSet sideEffectSet) { + nativeAccess.applySideEffects(position, previousType, sideEffectSet); + return Sets.intersection(NeoForgeWorldEdit.inst.getPlatform().getSupportedSideEffects(), sideEffectSet.getSideEffectsToApply()); + } + + @Override + public int getBlockLightLevel(BlockVector3 position) { + checkNotNull(position); + return getWorld().getLightEmission(NeoForgeAdapter.toBlockPos(position)); + } + + @Override + public boolean clearContainerBlockContents(BlockVector3 position) { + checkNotNull(position); + + BlockEntity tile = getWorld().getBlockEntity(NeoForgeAdapter.toBlockPos(position)); + if (tile instanceof Clearable) { + ((Clearable) tile).clearContent(); + return true; + } + return false; + } + + @Override + public BiomeType getBiome(BlockVector3 position) { + checkNotNull(position); + + LevelChunk chunk = getWorld().getChunk(position.x() >> 4, position.z() >> 4); + return getBiomeInChunk(position, chunk); + } + + private BiomeType getBiomeInChunk(BlockVector3 position, ChunkAccess chunk) { + return NeoForgeAdapter.adapt( + chunk.getNoiseBiome(position.x() >> 2, position.y() >> 2, position.z() >> 2).value() + ); + } + + @Override + public boolean setBiome(BlockVector3 position, BiomeType biome) { + checkNotNull(position); + checkNotNull(biome); + + LevelChunk chunk = getWorld().getChunk(position.x() >> 4, position.z() >> 4); + var biomes = (PalettedContainer>) chunk.getSection(chunk.getSectionIndex(position.y())).getBiomes(); + biomes.getAndSetUnchecked( + position.x() & 3, position.y() & 3, position.z() & 3, + getWorld().registryAccess().lookupOrThrow(Registries.BIOME) + .getOrThrow(ResourceKey.create(Registries.BIOME, ResourceLocation.parse(biome.id()))) + ); + chunk.markUnsaved(); + return true; + } + + private static final LoadingCache fakePlayers + = CacheBuilder.newBuilder().weakKeys().softValues().build(CacheLoader.from(NeoForgeFakePlayer::new)); + + @Override + public boolean useItem(BlockVector3 position, BaseItem item, Direction face) { + ItemStack stack = NeoForgeAdapter.adapt(new BaseItemStack(item.getType(), item.getNbtReference(), 1)); + ServerLevel world = getWorld(); + final NeoForgeFakePlayer fakePlayer; + try { + fakePlayer = fakePlayers.get(world); + } catch (ExecutionException ignored) { + return false; + } + fakePlayer.setItemInHand(InteractionHand.MAIN_HAND, stack); + fakePlayer.absSnapTo(position.x(), position.y(), position.z(), + (float) face.toVector().toYaw(), (float) face.toVector().toPitch()); + final BlockPos blockPos = NeoForgeAdapter.toBlockPos(position); + final BlockHitResult rayTraceResult = new BlockHitResult(NeoForgeAdapter.toVec3(position), + NeoForgeAdapter.adapt(face), blockPos, false); + UseOnContext itemUseContext = new UseOnContext(fakePlayer, InteractionHand.MAIN_HAND, rayTraceResult); + InteractionResult used = stack.onItemUseFirst(itemUseContext); + if (used != InteractionResult.SUCCESS) { + // try activating the block + InteractionResult resultType = getWorld().getBlockState(blockPos).useItemOn(stack, world, fakePlayer, InteractionHand.MAIN_HAND, rayTraceResult); + if (resultType.consumesAction()) { + used = resultType; + } else { + used = stack.getItem().use(world, fakePlayer, InteractionHand.MAIN_HAND); + } + } + return used == InteractionResult.SUCCESS; + } + + @Override + public void dropItem(Vector3 position, BaseItemStack item) { + checkNotNull(position); + checkNotNull(item); + + if (item.getType() == ItemTypes.AIR) { + return; + } + + ItemEntity entity = new ItemEntity(getWorld(), position.x(), position.y(), position.z(), NeoForgeAdapter.adapt(item)); + entity.setPickUpDelay(10); + getWorld().addFreshEntity(entity); + } + + @Override + public void simulateBlockMine(BlockVector3 position) { + BlockPos pos = NeoForgeAdapter.toBlockPos(position); + getWorld().destroyBlock(pos, true); + } + + @Override + public boolean canPlaceAt(BlockVector3 position, BlockState blockState) { + return NeoForgeAdapter.adapt(blockState).canSurvive(getWorld(), NeoForgeAdapter.toBlockPos(position)); + } + + // For unmapped regen names, see Fabric! + + @Override + public boolean regenerate(Region region, Extent extent, RegenOptions options) { + try { + doRegen(region, extent, options); + } catch (Exception e) { + throw new IllegalStateException("Regen failed", e); + } + + return true; + } + + private void doRegen(Region region, Extent extent, RegenOptions options) throws Exception { + Path tempDir = Files.createTempDirectory("WorldEditWorldGen"); + LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir); + try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("WorldEditTempGen")) { + ServerLevel originalWorld = getWorld(); + PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer() + .getWorldData().overworldData(); + WorldOptions originalOpts = levelProperties.worldGenOptions(); + + long seed = options.getSeed().orElse(originalWorld.getSeed()); + + levelProperties.worldOptions = options.getSeed().isPresent() + ? originalOpts.withSeed(OptionalLong.of(seed)) + : originalOpts; + + ResourceKey worldRegKey = originalWorld.dimension(); + try (ServerLevel serverWorld = new ServerLevel( + originalWorld.getServer(), Util.backgroundExecutor(), session, + ((ServerLevelData) originalWorld.getLevelData()), + worldRegKey, + new LevelStem( + originalWorld.dimensionTypeRegistration(), + originalWorld.getChunkSource().getGenerator() + ), + originalWorld.isDebug(), + seed, + // No spawners are needed for this world. + ImmutableList.of(), + // This controls ticking, we don't need it so set it to false. + false, + originalWorld.getRandomSequences() + )) { + regenForWorld(region, extent, serverWorld, options); + + // drive the server executor until all tasks are popped off + while (originalWorld.getServer().pollTask()) { + Thread.yield(); + } + } finally { + levelProperties.worldOptions = originalOpts; + } + } finally { + SafeFiles.tryHardToDeleteDir(tempDir); + } + } + + private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld, + RegenOptions options) throws WorldEditException { + List> chunkLoadings = submitChunkLoadTasks(region, serverWorld); + + // drive executor until loading finishes + BlockableEventLoop executor = serverWorld.getChunkSource().mainThreadProcessor; + executor.managedBlock(() -> { + // bail out early if a future fails + if (chunkLoadings.stream().anyMatch(ftr -> + ftr.isDone() && Futures.getUnchecked(ftr) == null + )) { + return false; + } + return chunkLoadings.stream().allMatch(CompletableFuture::isDone); + }); + + Map chunks = new HashMap<>(); + for (CompletableFuture future : chunkLoadings) { + @Nullable + ChunkAccess chunk = future.getNow(null); + checkState(chunk != null, "Failed to generate a chunk, regen failed."); + chunks.put(chunk.getPos(), chunk); + } + + for (BlockVector3 vec : region) { + BlockPos pos = NeoForgeAdapter.toBlockPos(vec); + ChunkAccess chunk = chunks.get(new ChunkPos(pos)); + BlockStateHolder state = NeoForgeAdapter.adapt(chunk.getBlockState(pos)); + BlockEntity blockEntity = chunk.getBlockEntity(pos); + if (blockEntity != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, getWorld().registryAccess()); + blockEntity.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + state = state.toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(tag))); + } + extent.setBlock(vec, state.toBaseBlock()); + + if (options.shouldRegenBiomes()) { + BiomeType biome = getBiomeInChunk(vec, chunk); + extent.setBiome(vec, biome); + } + } + } + + private List> submitChunkLoadTasks(Region region, ServerLevel world) { + List> chunkLoadings = new ArrayList<>(); + // Pre-gen all the chunks + for (BlockVector2 chunk : region.getChunks()) { + chunkLoadings.add( + world.getChunkSource().getChunkFuture(chunk.x(), chunk.z(), ChunkStatus.FEATURES, true) + .thenApply(either -> either.orElse(null)) + ); + } + return chunkLoadings; + } + + @SuppressWarnings("deprecation") + @Nullable + private static ResourceKey createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType type) { + return switch (type) { + // Based off of the SaplingGenerator class, as well as uses of DefaultBiomeFeatures fields + case TREE -> TreePlacements.OAK_CHECKED; + case BIG_TREE -> TreePlacements.FANCY_OAK_CHECKED; + case REDWOOD -> TreePlacements.SPRUCE_CHECKED; + case TALL_REDWOOD -> TreePlacements.MEGA_SPRUCE_CHECKED; + case MEGA_REDWOOD -> TreePlacements.MEGA_PINE_CHECKED; + case BIRCH -> TreePlacements.BIRCH_CHECKED; + case JUNGLE -> TreePlacements.MEGA_JUNGLE_TREE_CHECKED; + case SMALL_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case SHORT_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case JUNGLE_BUSH -> TreePlacements.JUNGLE_BUSH; + case SWAMP -> TreePlacements.OAK_CHECKED; + case ACACIA -> TreePlacements.ACACIA_CHECKED; + case DARK_OAK -> TreePlacements.DARK_OAK_CHECKED; + case TALL_BIRCH -> TreePlacements.SUPER_BIRCH_BEES_0002; + case WARPED_FUNGUS -> TreePlacements.WARPED_FUNGI; + case CRIMSON_FUNGUS -> TreePlacements.CRIMSON_FUNGI; + case CHORUS_PLANT -> EndPlacements.CHORUS_PLANT; + case MANGROVE -> TreePlacements.MANGROVE_CHECKED; + case TALL_MANGROVE -> TreePlacements.TALL_MANGROVE_CHECKED; + case CHERRY -> TreePlacements.CHERRY_CHECKED; + case PALE_OAK -> TreePlacements.PALE_OAK_CHECKED; + case PALE_OAK_CREAKING -> TreePlacements.PALE_OAK_CREAKING_CHECKED; + case RANDOM -> createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType.values()[ThreadLocalRandom.current().nextInt(com.sk89q.worldedit.util.TreeGenerator.TreeType.values().length)]); + default -> null; + }; + } + + @SuppressWarnings("deprecation") + @Override + public boolean generateTree(com.sk89q.worldedit.util.TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { + ServerLevel world = getWorld(); + PlacedFeature generator = Optional.ofNullable(createTreeFeatureGenerator(type)) + .map(k -> world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(k)) + .orElse(null); + ServerChunkCache chunkManager = world.getChunkSource(); + if (type == com.sk89q.worldedit.util.TreeGenerator.TreeType.CHORUS_PLANT) { + position = position.add(0, 1, 0); + } + try (NeoForgeServerLevelDelegateProxy.LevelAndProxy proxyLevel = NeoForgeServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + NeoForgeAdapter.toBlockPos(position) + ); + } + } + + @Override + public boolean generateTree(com.sk89q.worldedit.world.generation.TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { + ServerLevel world = getWorld(); + PlacedFeature generator = world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ServerChunkCache chunkManager = world.getChunkSource(); + try (NeoForgeServerLevelDelegateProxy.LevelAndProxy proxyLevel = NeoForgeServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + NeoForgeAdapter.toBlockPos(position) + ); + } + } + + @Override + public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = getWorld(); + ConfiguredFeature feature = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ServerChunkCache chunkManager = world.getChunkSource(); + try (NeoForgeServerLevelDelegateProxy.LevelAndProxy levelProxy = + NeoForgeServerLevelDelegateProxy.newInstance(editSession, world)) { + return feature != null && feature.place(levelProxy.level(), chunkManager.getGenerator(), random, NeoForgeAdapter.toBlockPos(position)); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean generateStructure(StructureType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = getWorld(); + Registry structureRegistry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); + Structure structure = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); + if (structure == null) { + return false; + } + + ServerChunkCache chunkManager = world.getChunkSource(); + try (NeoForgeServerLevelDelegateProxy.LevelAndProxy levelProxy = + NeoForgeServerLevelDelegateProxy.newInstance(editSession, world)) { + ChunkPos chunkPos = new ChunkPos(new BlockPos(position.x(), position.y(), position.z())); + StructureStart structureStart = structure.generate( + structureRegistry.wrapAsHolder(structure), world.dimension(), world.registryAccess(), + chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), + world.getStructureManager(), world.getSeed(), chunkPos, 0, levelProxy.level(), + biome -> true + ); + + if (!structureStart.isValid()) { + return false; + } else { + BoundingBox boundingBox = structureStart.getBoundingBox(); + ChunkPos min = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.minX()), SectionPos.blockToSectionCoord(boundingBox.minZ())); + ChunkPos max = new ChunkPos(SectionPos.blockToSectionCoord(boundingBox.maxX()), SectionPos.blockToSectionCoord(boundingBox.maxZ())); + ChunkPos.rangeClosed(min, max).forEach((chunkPosx) -> + structureStart.placeInChunk( + levelProxy.level(), world.structureManager(), chunkManager.getGenerator(), world.getRandom(), + new BoundingBox( + chunkPosx.getMinBlockX(), world.getMinY(), chunkPosx.getMinBlockZ(), + chunkPosx.getMaxBlockX(), world.getMaxY(), chunkPosx.getMaxBlockZ() + ), + chunkPosx + ) + ); + return true; + } + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } + } + + @Override + public void checkLoadedChunk(BlockVector3 pt) { + getWorld().getChunk(NeoForgeAdapter.toBlockPos(pt)); + } + + @Override + public void fixAfterFastMode(Iterable chunks) { + fixLighting(chunks); + } + + @Override + public void sendBiomeUpdates(Iterable chunks) { + List nativeChunks = chunks instanceof Collection chunkCollection ? Lists.newArrayListWithCapacity(chunkCollection.size()) : Lists.newArrayList(); + for (BlockVector2 chunk : chunks) { + nativeChunks.add(getWorld().getChunk(chunk.x(), chunk.z(), ChunkStatus.BIOMES, false)); + } + getWorld().getChunkSource().chunkMap.resendBiomesForChunks(nativeChunks); + } + + @Override + public void fixLighting(Iterable chunks) { + ServerLevel world = getWorld(); + for (BlockVector2 chunk : chunks) { + // Fetch the chunk after light initialization at least + // We'll be doing a full relight anyways, so we don't need to be LIGHT yet + world.getChunkSource().getLightEngine().lightChunk(world.getChunk( + chunk.x(), chunk.z(), ChunkStatus.INITIALIZE_LIGHT + ), false); + } + } + + @Override + public WeatherType getWeather() { + LevelData info = getWorld().getLevelData(); + if (info.isThundering()) { + return WeatherTypes.THUNDER_STORM; + } + if (info.isRaining()) { + return WeatherTypes.RAIN; + } + return WeatherTypes.CLEAR; + } + + @Override + public long getRemainingWeatherDuration() { + ServerLevelData info = (ServerLevelData) getWorld().getLevelData(); + if (info.isThundering()) { + return info.getThunderTime(); + } + if (info.isRaining()) { + return info.getRainTime(); + } + return info.getClearWeatherTime(); + } + + @Override + public void setWeather(WeatherType weatherType) { + setWeather(weatherType, 0); + } + + @Override + public void setWeather(WeatherType weatherType, long duration) { + ServerLevelData info = (ServerLevelData) getWorld().getLevelData(); + if (weatherType == WeatherTypes.THUNDER_STORM) { + info.setClearWeatherTime(0); + info.setThundering(true); + info.setThunderTime((int) duration); + } else if (weatherType == WeatherTypes.RAIN) { + info.setClearWeatherTime(0); + info.setRaining(true); + info.setRainTime((int) duration); + } else if (weatherType == WeatherTypes.CLEAR) { + info.setRaining(false); + info.setThundering(false); + info.setClearWeatherTime((int) duration); + } + } + + @Override + public int getMinY() { + return getWorld().getMinY(); + } + + @Override + public int getMaxY() { + return getWorld().getMaxY(); + } + + @Override + public BlockVector3 getSpawnPosition() { + return NeoForgeAdapter.adapt(getWorld().getLevelData().getRespawnData().pos()); + } + + @Override + public BlockState getBlock(BlockVector3 position) { + net.minecraft.world.level.block.state.BlockState mcState = getWorld() + .getChunk(position.x() >> 4, position.z() >> 4) + .getBlockState(NeoForgeAdapter.toBlockPos(position)); + + return NeoForgeAdapter.adapt(mcState); + } + + @Override + public BaseBlock getFullBlock(BlockVector3 position) { + BlockPos pos = new BlockPos(position.x(), position.y(), position.z()); + BlockEntity tile = getWorld().getChunk(pos).getBlockEntity(pos); + + if (tile != null) { + var tagValueOutput = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, getWorld().registryAccess()); + tile.saveWithId(tagValueOutput); + net.minecraft.nbt.CompoundTag tag = tagValueOutput.buildResult(); + return getBlock(position).toBaseBlock( + LazyReference.from(() -> NBTConverter.fromNative(tag)) + ); + } else { + return getBlock(position).toBaseBlock(); + } + } + + @Override + public int hashCode() { + return getWorld().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } else if ((o instanceof NeoForgeWorld other)) { + Level otherWorld = other.worldRef.get(); + Level thisWorld = worldRef.get(); + return otherWorld != null && otherWorld.equals(thisWorld); + } else if (o instanceof com.sk89q.worldedit.world.World) { + return ((com.sk89q.worldedit.world.World) o).getName().equals(getName()); + } else { + return false; + } + } + + @Override + public List getEntities(Region region) { + final ServerLevel world = getWorld(); + AABB box = new AABB( + NeoForgeAdapter.toVec3(region.getMinimumPoint()), + NeoForgeAdapter.toVec3(region.getMaximumPoint().add(BlockVector3.ONE)) + ); + List nmsEntities = world.getEntities( + (net.minecraft.world.entity.Entity) null, + box, + e -> region.contains(NeoForgeAdapter.adapt(e.blockPosition())) + ); + return nmsEntities.stream().map(NeoForgeEntity::new).collect(ImmutableList.toImmutableList()); + } + + @Override + public List getEntities() { + final ServerLevel world = getWorld(); + return Streams.stream(world.getAllEntities()) + .map(NeoForgeEntity::new) + .collect(ImmutableList.toImmutableList()); + } + + @Nullable + @Override + public Entity createEntity(Location location, BaseEntity entity) { + ServerLevel world = getWorld(); + String entityId = entity.getType().id(); + final Optional> entityType = EntityType.byString(entityId); + if (entityType.isEmpty()) { + return null; + } + LinCompoundTag nativeTag = entity.getNbt(); + net.minecraft.nbt.CompoundTag tag; + if (nativeTag != null) { + tag = NBTConverter.toNative(nativeTag); + removeUnwantedEntityTagsRecursively(tag); + } else { + tag = new net.minecraft.nbt.CompoundTag(); + } + tag.putString("id", entityId); + + net.minecraft.world.entity.Entity createdEntity = EntityType.loadEntityRecursive(tag, world, EntitySpawnReason.COMMAND, (loadedEntity) -> { + loadedEntity.absSnapTo(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + return loadedEntity; + }); + if (createdEntity != null) { + world.addFreshEntityWithPassengers(createdEntity); + return new NeoForgeEntity(createdEntity); + } + return null; + } + + private void removeUnwantedEntityTagsRecursively(net.minecraft.nbt.CompoundTag tag) { + for (String name : Constants.NO_COPY_ENTITY_NBT_FIELDS) { + tag.remove(name); + } + + // Adapted from net.minecraft.world.entity.EntityType#loadEntityRecursive + tag.getList("Passengers").ifPresent(nbttaglist -> { + for (int i = 0; i < nbttaglist.size(); ++i) { + removeUnwantedEntityTagsRecursively(nbttaglist.getCompoundOrEmpty(i)); + } + }); + } + + @Override + public Mask createLiquidMask() { + return new AbstractExtentMask(this) { + @Override + public boolean test(BlockVector3 vector) { + return NeoForgeAdapter.adapt(getExtent().getBlock(vector)).getBlock() instanceof LiquidBlock; + } + }; + } + + @Override + public boolean isValid() { + return worldRef.get() != null; + } +} diff --git a/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorldEdit.java b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorldEdit.java new file mode 100644 index 0000000000..fcb84a283a --- /dev/null +++ b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorldEdit.java @@ -0,0 +1,501 @@ +/* + * 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.neoforge; + +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.command.util.PermissionCondition; +import com.sk89q.worldedit.event.platform.PlatformReadyEvent; +import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; +import com.sk89q.worldedit.event.platform.PlatformsRegisteredEvent; +import com.sk89q.worldedit.event.platform.SessionIdleEvent; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extension.platform.PlatformManager; +import com.sk89q.worldedit.internal.anvil.ChunkDeleter; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.Direction; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.world.biome.BiomeCategory; +import com.sk89q.worldedit.world.biome.BiomeType; +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.gamemode.GameModes; +import com.sk89q.worldedit.world.generation.ConfiguredFeatureType; +import com.sk89q.worldedit.world.generation.StructureType; +import com.sk89q.worldedit.world.generation.TreeType; +import com.sk89q.worldedit.world.item.ItemCategory; +import com.sk89q.worldedit.world.item.ItemType; +import com.sk89q.worldedit.world.weather.WeatherTypes; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.FallenTreeFeature; +import net.minecraft.world.level.levelgen.feature.TreeFeature; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.CommandEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import org.apache.logging.log4j.Logger; +import org.enginehub.piston.Command; +import org.enginehub.worldeditcui.protocol.CUIPacket; +import org.enginehub.worldeditcui.protocol.CUIPacketHandler; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME; +import static com.sk89q.worldedit.neoforge.NeoForgeAdapter.adaptCommandSource; +import static com.sk89q.worldedit.neoforge.NeoForgeAdapter.adaptPlayer; + +/** + * The Forge implementation of WorldEdit. + */ +@Mod(NeoForgeWorldEdit.MOD_ID) +public class NeoForgeWorldEdit { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + public static final String MOD_ID = "worldedit"; + + private NeoForgePermissionsProvider provider; + + public static NeoForgeWorldEdit inst; + + private InteractionDebouncer debouncer; + private NeoForgePlatform platform; + private NeoForgeConfiguration config; + private Path workingDir; + + private ModContainer container; + + public NeoForgeWorldEdit(IEventBus modBus) { + inst = this; + + modBus.addListener(this::init); + + NeoForge.EVENT_BUS.register(ThreadSafeCache.getInstance()); + NeoForge.EVENT_BUS.register(this); + } + + private void init(FMLCommonSetupEvent event) { + this.container = ModLoadingContext.get().getActiveContainer(); + + // Setup working directory + workingDir = FMLPaths.CONFIGDIR.get().resolve("worldedit"); + if (!Files.exists(workingDir)) { + try { + Files.createDirectory(workingDir); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + CUIPacketHandler.instance().registerServerboundHandler(this::onCuiPacket); + + setupPlatform(); + + LOGGER.info("WorldEdit for NeoForge (version {}) is loaded", getInternalVersion()); + } + + private void setupPlatform() { + this.platform = new NeoForgePlatform(this); + debouncer = new InteractionDebouncer(platform); + + WorldEdit.getInstance().getPlatformManager().register(platform); + + config = new NeoForgeConfiguration(this); + + this.provider = new NeoForgePermissionsProvider.VanillaPermissionsProvider(platform); + } + + private void setupRegistries(MinecraftServer server) { + // Blocks + for (ResourceLocation name : BuiltInRegistries.BLOCK.keySet()) { + String key = name.toString(); + if (BlockType.REGISTRY.get(key) == null) { + BlockType.REGISTRY.register(key, new BlockType(key, + input -> NeoForgeAdapter.adapt(NeoForgeAdapter.adapt(input.getBlockType()).defaultBlockState()))); + } + } + // Items + for (ResourceLocation name : BuiltInRegistries.ITEM.keySet()) { + String key = name.toString(); + if (ItemType.REGISTRY.get(key) == null) { + ItemType.REGISTRY.register(key, new ItemType(key)); + } + } + // Entities + for (ResourceLocation name : BuiltInRegistries.ENTITY_TYPE.keySet()) { + String key = name.toString(); + if (EntityType.REGISTRY.get(key) == null) { + EntityType.REGISTRY.register(key, new EntityType(key)); + } + } + // Biomes + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.BIOME).keySet()) { + String key = name.toString(); + if (BiomeType.REGISTRY.get(key) == null) { + BiomeType.REGISTRY.register(key, new BiomeType(key)); + } + } + // Tags + server.registryAccess().lookupOrThrow(Registries.BLOCK).getTags().map(t -> t.key().location()).forEach(name -> { + String key = name.toString(); + if (BlockCategory.REGISTRY.get(key) == null) { + BlockCategory.REGISTRY.register(key, new BlockCategory(key)); + } + }); + server.registryAccess().lookupOrThrow(Registries.ITEM).getTags().map(t -> t.key().location()).forEach(name -> { + String key = name.toString(); + if (ItemCategory.REGISTRY.get(key) == null) { + ItemCategory.REGISTRY.register(key, new ItemCategory(key)); + } + }); + Registry biomeRegistry = server.registryAccess().lookupOrThrow(Registries.BIOME); + biomeRegistry.getTags().forEach(tag -> { + String key = tag.key().location().toString(); + if (BiomeCategory.REGISTRY.get(key) == null) { + BiomeCategory.REGISTRY.register(key, new BiomeCategory( + key, + () -> biomeRegistry.get(tag.key()) + .stream() + .flatMap(HolderSet.Named::stream) + .map(Holder::value) + .map(NeoForgeAdapter::adapt) + .collect(Collectors.toSet())) + ); + } + }); + // Features + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).keySet()) { + String key = name.toString(); + if (ConfiguredFeatureType.REGISTRY.get(key) == null) { + ConfiguredFeatureType.REGISTRY.register(key, new ConfiguredFeatureType(key)); + } + } + // Structures + for (ResourceLocation name : server.registryAccess().lookupOrThrow(Registries.STRUCTURE).keySet()) { + String key = name.toString(); + if (StructureType.REGISTRY.get(key) == null) { + StructureType.REGISTRY.register(key, new StructureType(key)); + } + } + // Trees + Registry placedFeatureRegistry = server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + for (ResourceLocation name : placedFeatureRegistry.keySet()) { + // Do some hackery to make sure this is a tree + var underlyingFeature = placedFeatureRegistry.get(name).get().value().feature().value().feature(); + if (underlyingFeature instanceof TreeFeature || underlyingFeature instanceof FallenTreeFeature || underlyingFeature instanceof CoralTreeFeature) { + String key = name.toString(); + if (TreeType.REGISTRY.get(key) == null) { + TreeType.REGISTRY.register(key, new TreeType(key)); + } + } + } + + // ... :| + GameModes.get(""); + WeatherTypes.get(""); + com.sk89q.worldedit.registry.Registries.get(""); + } + + @SubscribeEvent + public void registerCommands(RegisterCommandsEvent event) { + WorldEdit.getInstance().getEventBus().post(new PlatformsRegisteredEvent()); + + PlatformManager manager = WorldEdit.getInstance().getPlatformManager(); + Platform commandsPlatform = manager.queryCapability(Capability.USER_COMMANDS); + if (commandsPlatform != platform || !platform.isHookingEvents()) { + // We're not in control of commands/events -- do not register. + return; + } + + List commands = manager.getPlatformCommandManager().getCommandManager() + .getAllCommands().toList(); + for (Command command : commands) { + CommandWrapper.register(event.getDispatcher(), command); + Set perms = command.getCondition().as(PermissionCondition.class) + .map(PermissionCondition::getPermissions) + .orElseGet(Collections::emptySet); + if (!perms.isEmpty()) { + perms.forEach(getPermissionsProvider()::registerPermission); + } + } + } + + @SubscribeEvent + public void serverAboutToStart(ServerAboutToStartEvent event) { + final Path delChunks = workingDir.resolve(DELCHUNKS_FILE_NAME); + if (Files.exists(delChunks)) { + ChunkDeleter.runFromFile(delChunks, true); + } + } + + @SubscribeEvent + public void serverStopping(ServerStoppingEvent event) { + WorldEdit worldEdit = WorldEdit.getInstance(); + worldEdit.getSessionManager().unload(); + WorldEdit.getInstance().getEventBus().post(new PlatformUnreadyEvent(platform)); + } + + @SubscribeEvent + public void serverStarted(ServerStartedEvent event) { + setupRegistries(event.getServer()); + + config.load(); + WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform)); + } + + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } + + private boolean skipInteractionEvent(Player player, InteractionHand hand) { + return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide() || !(player instanceof ServerPlayer); + } + + @SubscribeEvent + public void onLeftClickBlock(PlayerInteractEvent.LeftClickBlock event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem().isFalse()) { + return; + } + + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + NeoForgePlayer player = adaptPlayer(playerEntity); + NeoForgeWorld world = getWorld(playerEntity.level()); + Direction direction = NeoForgeAdapter.adaptEnumFacing(event.getFace()); + + BlockPos blockPos = event.getPos(); + Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + + boolean result = we.handleBlockLeftClick(player, pos, direction) || we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem().isFalse()) { + return; + } + + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + NeoForgePlayer player = adaptPlayer(playerEntity); + NeoForgeWorld world = getWorld(playerEntity.level()); + Direction direction = NeoForgeAdapter.adaptEnumFacing(event.getFace()); + + BlockPos blockPos = event.getPos(); + Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + + boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCanceled(true); + } + } + + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand)) { + return; + } + + WorldEdit we = WorldEdit.getInstance(); + NeoForgePlayer player = adaptPlayer(playerEntity); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return; + } + + boolean result = we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); + } + + @SubscribeEvent + public void onRightClickItem(PlayerInteractEvent.RightClickItem event) { + if (skipInteractionEvent(event.getEntity(), event.getHand())) { + return; + } + + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + NeoForgePlayer player = adaptPlayer(playerEntity); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + if (previousResult.get()) { + event.setCanceled(true); + } + return; + } + + boolean result = we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onCommandEvent(CommandEvent event) throws CommandSyntaxException { + ParseResults parseResults = event.getParseResults(); + if (parseResults.getContext().getSource().getEntity() instanceof ServerPlayer player && player.level().isClientSide()) { + return; + } + if (parseResults.getContext().getCommand() != CommandWrapper.FAKE_COMMAND) { + return; + } + event.setCanceled(true); + WorldEdit.getInstance().getEventBus().post(new com.sk89q.worldedit.event.platform.CommandEvent( + adaptCommandSource(parseResults.getContext().getSource()), + "/" + parseResults.getReader().getString() + )); + } + + @SubscribeEvent + public void onPlayerLogOut(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + debouncer.clearInteraction(adaptPlayer(player)); + + WorldEdit.getInstance().getEventBus() + .post(new SessionIdleEvent(new NeoForgePlayer.SessionKeyImpl(player))); + } + } + + private void onCuiPacket(CUIPacket payload, CUIPacketHandler.PacketContext context) { + if (!(context.player() instanceof ServerPlayer player)) { + // Ignore - this is not a server-bound packet + return; + } + NeoForgePlayer actor = NeoForgeAdapter.adaptPlayer(player); + LocalSession session = WorldEdit.getInstance().getSessionManager().get(actor); + session.handleCUIInitializationMessage(payload.eventType(), payload.args(), actor); + } + + /** + * Get the configuration. + * + * @return the Forge configuration + */ + NeoForgeConfiguration getConfig() { + return this.config; + } + + /** + * Get the session for a player. + * + * @param player the player + * @return the session + */ + public LocalSession getSession(ServerPlayer player) { + checkNotNull(player); + return WorldEdit.getInstance().getSessionManager().get(adaptPlayer(player)); + } + + /** + * Get the WorldEdit proxy for the given world. + * + * @param world the world + * @return the WorldEdit world + */ + public NeoForgeWorld getWorld(ServerLevel world) { + checkNotNull(world); + return new NeoForgeWorld(world); + } + + /** + * Get the WorldEdit proxy for the platform. + * + * @return the WorldEdit platform + */ + public Platform getPlatform() { + return this.platform; + } + + /** + * Get the working directory where WorldEdit's files are stored. + * + * @return the working directory + */ + public Path getWorkingDir() { + return this.workingDir; + } + + /** + * Get the version of the WorldEdit-for-Forge implementation. + * + * @return a version string + */ + String getInternalVersion() { + return container.getModInfo().getVersion().toString(); + } + + public void setPermissionsProvider(NeoForgePermissionsProvider provider) { + this.provider = provider; + } + + public NeoForgePermissionsProvider getPermissionsProvider() { + return provider; + } + +} diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorld.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorld.java index db945669af..fd654fc078 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorld.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorld.java @@ -19,93 +19,124 @@ package com.sk89q.worldedit.sponge; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.flowpowered.math.vector.Vector3d; -import com.flowpowered.math.vector.Vector3i; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.registry.state.Property; +import com.sk89q.worldedit.sponge.internal.NbtAdapter; +import com.sk89q.worldedit.sponge.internal.SpongeWorldNativeAccess; import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.SideEffect; +import com.sk89q.worldedit.util.SideEffectSet; +import com.sk89q.worldedit.util.TreeGenerator; import com.sk89q.worldedit.world.AbstractWorld; +import com.sk89q.worldedit.world.RegenOptions; +import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.biome.BiomeType; 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.TreeType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.weather.WeatherType; import com.sk89q.worldedit.world.weather.WeatherTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.placement.EndPlacements; +import net.minecraft.data.worldgen.placement.TreePlacements; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import org.apache.logging.log4j.Logger; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinIntTag; +import org.enginehub.linbus.tree.LinStringTag; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.Server; import org.spongepowered.api.Sponge; -import org.spongepowered.api.block.BlockSnapshot; -import org.spongepowered.api.block.BlockState; -import org.spongepowered.api.block.BlockType; -import org.spongepowered.api.block.BlockTypes; -import org.spongepowered.api.block.tileentity.TileEntity; -import org.spongepowered.api.data.key.Keys; -import org.spongepowered.api.data.property.block.GroundLuminanceProperty; -import org.spongepowered.api.data.property.block.SkyLuminanceProperty; +import org.spongepowered.api.block.entity.BlockEntity; +import org.spongepowered.api.block.entity.BlockEntityArchetype; +import org.spongepowered.api.block.entity.BlockEntityType; +import org.spongepowered.api.data.Keys; +import org.spongepowered.api.entity.EntityArchetype; import org.spongepowered.api.entity.EntityType; import org.spongepowered.api.entity.EntityTypes; +import org.spongepowered.api.entity.Item; +import org.spongepowered.api.registry.RegistryTypes; +import org.spongepowered.api.util.Ticks; import org.spongepowered.api.world.BlockChangeFlags; -import org.spongepowered.api.world.World; -import org.spongepowered.api.world.weather.Weather; +import org.spongepowered.api.world.LightTypes; +import org.spongepowered.api.world.SerializationBehavior; +import org.spongepowered.api.world.generation.config.WorldGenerationConfig; +import org.spongepowered.api.world.server.ServerLocation; +import org.spongepowered.api.world.server.ServerWorld; +import org.spongepowered.api.world.server.WorldArchetype; +import org.spongepowered.api.world.server.WorldArchetypeType; +import org.spongepowered.api.world.server.storage.ServerWorldProperties; +import org.spongepowered.api.world.volume.stream.StreamOptions; +import org.spongepowered.math.vector.Vector3d; +import org.spongepowered.math.vector.Vector3i; import java.lang.ref.WeakReference; import java.nio.file.Path; -import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Optional; - +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkNotNull; + /** * An adapter to Minecraft worlds for WorldEdit. */ -public abstract class SpongeWorld extends AbstractWorld { +public final class SpongeWorld extends AbstractWorld { + + private static final RandomSource random = RandomSource.create(); + private static final Logger LOGGER = LogManagerCompat.getLogger(); - private final WeakReference worldRef; + private final WeakReference worldRef; + private final SpongeWorldNativeAccess worldNativeAccess; /** * Construct a new world. * * @param world the world */ - protected SpongeWorld(World world) { + SpongeWorld(ServerWorld world) { checkNotNull(world); this.worldRef = new WeakReference<>(world); + this.worldNativeAccess = new SpongeWorldNativeAccess(new WeakReference<>((ServerLevel) world)); } /** * Get the underlying handle to the world. * * @return the world - * @throws WorldEditException thrown if a reference to the world was lost (i.e. world was unloaded) - */ - public World getWorldChecked() throws WorldEditException { - World world = worldRef.get(); - if (world != null) { - return world; - } else { - throw new WorldReferenceLostException("The reference to the world was lost (i.e. the world may have been unloaded)"); - } - } - - /** - * Get the underlying handle to the world. - * - * @return the world - * @throws RuntimeException thrown if a reference to the world was lost (i.e. world was unloaded) + * @throws RuntimeException thrown if a reference to the world was lost (i.e. world was + * unloaded) */ - public World getWorld() { - World world = worldRef.get(); + ServerWorld getWorld() { + ServerWorld world = worldRef.get(); if (world != null) { return world; } else { @@ -113,109 +144,272 @@ public World getWorld() { } } + // This is sus but leaving it for later world name/id reworks @Override public String getName() { - return getWorld().getName(); + return getWorld().key().asString(); } @Override - public String getId() { - return getName().replace(" ", "_").toLowerCase(Locale.ROOT) + - getWorld().getDimension().getType().getName().toLowerCase(Locale.ROOT); + public String id() { + return getWorld().key().asString(); } @Override public Path getStoragePath() { - return getWorld().getDirectory(); + return getWorld().directory(); } - @SuppressWarnings("WeakerAccess") - protected BlockState getBlockState(BlockStateHolder block) { - if (block instanceof com.sk89q.worldedit.world.block.BlockState) { - BlockState state = Sponge.getRegistry().getType(BlockType.class, block.getBlockType().getId()).orElse(BlockTypes.AIR).getDefaultState(); - for (Map.Entry, Object> entry : block.getStates().entrySet()) { - // TODO Convert across states - } - return state; - } else { - throw new UnsupportedOperationException("Missing Sponge adapter for WorldEdit!"); + @Override + public BlockState getBlock(BlockVector3 position) { + return SpongeAdapter.adapt(getWorld().block( + position.x(), position.y(), position.z() + )); + } + + @Override + public BaseBlock getFullBlock(BlockVector3 position) { + BlockEntity blockEntity = getWorld().blockEntity( + position.x(), position.y(), position.z() + ).orElse(null); + LinCompoundTag blockEntityData = null; + if (blockEntity != null) { + BlockEntityArchetype blockEntityArchetype = blockEntity.createArchetype(); + BlockEntityType blockEntityType = blockEntityArchetype.blockEntityType(); + ResourceKey blockEntityId = blockEntityType.key(RegistryTypes.BLOCK_ENTITY_TYPE); + blockEntityData = NbtAdapter.adaptToWorldEdit(blockEntityArchetype.blockEntityData()); + + // Add ID and position since Sponge's #blockEntityData does not save metadata + LinCompoundTag.Builder fullBlockEntityDataBuilder = blockEntityData.toBuilder(); + fullBlockEntityDataBuilder.put("id", LinStringTag.of(blockEntityId.formatted())); + fullBlockEntityDataBuilder.put("x", LinIntTag.of(position.x())); + fullBlockEntityDataBuilder.put("y", LinIntTag.of(position.y())); + fullBlockEntityDataBuilder.put("z", LinIntTag.of(position.z())); + blockEntityData = fullBlockEntityDataBuilder.build(); } + return getBlock(position).toBaseBlock(blockEntityData); } - @SuppressWarnings("WeakerAccess") - protected abstract void applyTileEntityData(TileEntity entity, BaseBlock block); + @Override + public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) throws WorldEditException { + checkNotNull(position); + checkNotNull(block); + + ServerWorld world = getWorld(); + + org.spongepowered.api.block.BlockState newState = SpongeAdapter.adapt(block.toImmutableState()); + + boolean didSet = world.setBlock( + position.x(), position.y(), position.z(), + newState, + BlockChangeFlags.NONE + .withUpdateNeighbors(sideEffects.shouldApply(SideEffect.NEIGHBORS)) + .withNotifyClients(true) + .withPhysics(sideEffects.shouldApply(SideEffect.UPDATE)) + .withNotifyObservers(sideEffects.shouldApply(SideEffect.UPDATE)) + .withLightingUpdates(sideEffects.shouldApply(SideEffect.LIGHTING)) + .withPathfindingUpdates(sideEffects.shouldApply(SideEffect.ENTITY_AI)) + .withNeighborDropsAllowed(false) + .withBlocksMoving(false) + .withForcedReRender(false) + .withIgnoreRender(false) + .withPerformBlockDestruction(false) + ); + if (!didSet) { + // still update NBT if the block is the same + if (world.block(position.x(), position.y(), position.z()) == newState) { + didSet = block.toBaseBlock().getNbt() != null; + } + } + + // Create the TileEntity + if (didSet && block instanceof BaseBlock baseBlock) { + LinCompoundTag nbt = baseBlock.getNbt(); + if (nbt != null) { + BlockEntityArchetype.builder() + .state(newState) + .blockEntity( + Sponge.game().registry(RegistryTypes.BLOCK_ENTITY_TYPE) + .value(ResourceKey.resolve(baseBlock.getNbtId())) + ) + .blockEntityData(NbtAdapter.adaptFromWorldEdit(nbt)) + .build() + .apply(ServerLocation.of(world, position.x(), position.y(), position.z())); + } + } - private static final BlockSnapshot.Builder builder = BlockSnapshot.builder(); + return true; + } @Override - public > boolean setBlock(BlockVector3 position, B block, boolean notifyAndLight) throws WorldEditException { + public Set applySideEffects(BlockVector3 position, com.sk89q.worldedit.world.block.BlockState previousType, SideEffectSet sideEffectSet) throws WorldEditException { checkNotNull(position); - checkNotNull(block); - World world = getWorldChecked(); + worldNativeAccess.applySideEffects(position, previousType, sideEffectSet); - // First set the block - Vector3i pos = new Vector3i(position.getX(), position.getY(), position.getZ()); - BlockState newState = getBlockState(block); + return Sets.intersection( + SpongeWorldEdit.inst().getInternalPlatform().getSupportedSideEffects(), + sideEffectSet.getSideEffectsToApply() + ); + } - BlockSnapshot snapshot = builder.reset() - .blockState(newState) - .position(pos) - .world(world.getProperties()) - .build(); + @Override + public boolean clearContainerBlockContents(BlockVector3 position) { + getWorld().removeBlockEntity(position.x(), position.y(), position.z()); + return true; + } - snapshot.restore(true, notifyAndLight ? BlockChangeFlags.ALL : BlockChangeFlags.NONE); + @Override + public boolean regenerate(Region region, Extent extent, RegenOptions options) { + Server server = Sponge.server(); + + final String id = "worldedittemp_" + getWorld().key().value(); + final ResourceKey key = ResourceKey.of("worldedit", id); + + WorldGenerationConfig worldGenConfig = WorldGenerationConfig.builder() + .from(getWorld().properties().worldGenerationConfig()) + .seed(options.getSeed().orElse(getWorld().properties().worldGenerationConfig().seed())) + .build(); + + WorldArchetypeType worldArchetypeType = WorldArchetypeType.builder() + .chunkGenerator(getWorld().generator()) + .worldType(getWorld().worldType()) + .build(); + + WorldArchetype worldArchetype = WorldArchetype.builder() + .generationConfig(worldGenConfig) + .type(worldArchetypeType) + .build(); + + ServerWorldProperties.LoadOptions loadOptions = ServerWorldProperties.LoadOptions.create( + worldArchetype, + (properties) -> { + properties.copyFrom(getWorld().properties()); + properties.offer(Keys.SERIALIZATION_BEHAVIOR, SerializationBehavior.NONE); + properties.offer(Keys.IS_LOAD_ON_STARTUP, false); + } + ); - // Create the TileEntity - if (block instanceof BaseBlock && ((BaseBlock) block).hasNbtData()) { - // Kill the old TileEntity - world.getTileEntity(pos).ifPresent(tileEntity -> applyTileEntityData(tileEntity, (BaseBlock) block)); + ServerWorld tempWorld; + try { + tempWorld = server.worldManager().loadWorld(key, loadOptions).get().get(); + } catch (InterruptedException | ExecutionException e) { + LOGGER.error("Failed to load temp world", e); + return false; + } + + try { + // Pre-gen all the chunks + // We need to also pull one more chunk in every direction + CuboidRegion expandedPreGen = new CuboidRegion(region.getMinimumPoint().subtract(16, 16, 16), region.getMaximumPoint().add(16, 16, 16)); + for (BlockVector3 chunk : expandedPreGen.getChunkCubes()) { + tempWorld.loadChunk(chunk.x(), chunk.y(), chunk.z(), true); + } + + World from = SpongeAdapter.adapt(tempWorld); + for (BlockVector3 vec : region) { + extent.setBlock(vec, from.getFullBlock(vec)); + if (options.shouldRegenBiomes()) { + extent.setBiome(vec, from.getBiome(vec)); + } + } + } catch (WorldEditException e) { + throw new RuntimeException(e); + } finally { + // Remove temp world + server.worldManager().unloadWorld(key).thenRun(() -> server.worldManager().deleteWorld(key)); } return true; } + + @SuppressWarnings("deprecation") + @Nullable + private static net.minecraft.resources.ResourceKey createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType type) { + return switch (type) { + // Based off of the SaplingGenerator class, as well as uses of DefaultBiomeFeatures fields + case TREE -> TreePlacements.OAK_CHECKED; + case BIG_TREE -> TreePlacements.FANCY_OAK_CHECKED; + case REDWOOD -> TreePlacements.SPRUCE_CHECKED; + case TALL_REDWOOD -> TreePlacements.MEGA_SPRUCE_CHECKED; + case MEGA_REDWOOD -> TreePlacements.MEGA_PINE_CHECKED; + case BIRCH -> TreePlacements.BIRCH_CHECKED; + case JUNGLE -> TreePlacements.MEGA_JUNGLE_TREE_CHECKED; + case SMALL_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case SHORT_JUNGLE -> TreePlacements.JUNGLE_TREE_CHECKED; + case JUNGLE_BUSH -> TreePlacements.JUNGLE_BUSH; + case SWAMP -> TreePlacements.OAK_CHECKED; + case ACACIA -> TreePlacements.ACACIA_CHECKED; + case DARK_OAK -> TreePlacements.DARK_OAK_CHECKED; + case TALL_BIRCH -> TreePlacements.SUPER_BIRCH_BEES_0002; + case WARPED_FUNGUS -> TreePlacements.WARPED_FUNGI; + case CRIMSON_FUNGUS -> TreePlacements.CRIMSON_FUNGI; + case CHORUS_PLANT -> EndPlacements.CHORUS_PLANT; + case MANGROVE -> TreePlacements.MANGROVE_CHECKED; + case TALL_MANGROVE -> TreePlacements.TALL_MANGROVE_CHECKED; + case CHERRY -> TreePlacements.CHERRY_CHECKED; + case PALE_OAK -> TreePlacements.PALE_OAK_CHECKED; + case PALE_OAK_CREAKING -> TreePlacements.PALE_OAK_CREAKING_CHECKED; + case RANDOM -> createTreeFeatureGenerator(com.sk89q.worldedit.util.TreeGenerator.TreeType.values()[ThreadLocalRandom.current().nextInt(com.sk89q.worldedit.util.TreeGenerator.TreeType.values().length)]); + default -> null; + }; + } + + @SuppressWarnings("deprecation") @Override - public boolean notifyAndLightBlock(BlockVector3 position, com.sk89q.worldedit.world.block.BlockState previousType) throws WorldEditException { - // TODO Move this to adapter - return false; + public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) { + ServerLevel world = (ServerLevel) getWorld(); + PlacedFeature generator = Optional.ofNullable(createTreeFeatureGenerator(type)) + .map(k -> world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(k)) + .orElse(null); + return generator != null && generator.place( + world, world.getChunkSource().getGenerator(), random, + new BlockPos(position.x(), position.y(), position.z()) + ); } @Override - public boolean regenerate(Region region, EditSession editSession) { - return false; + public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { + ServerLevel world = (ServerLevel) getWorld(); + PlacedFeature generator = world.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + return generator != null && generator.place( + world, world.getChunkSource().getGenerator(), random, + new BlockPos(position.x(), position.y(), position.z()) + ); } @Override public int getBlockLightLevel(BlockVector3 position) { checkNotNull(position); - BlockState state = getWorld().getBlock(new Vector3i(position.getX(), position.getY(), position.getZ())); - - Optional groundLuminanceProperty = state.getProperty(GroundLuminanceProperty.class); - Optional skyLuminanceProperty = state.getProperty(SkyLuminanceProperty.class); - - if (!groundLuminanceProperty.isPresent() || !skyLuminanceProperty.isPresent()) { - return 0; - } - - //noinspection ConstantConditions - return (int) Math.max(groundLuminanceProperty.get().getValue(), skyLuminanceProperty.get().getValue()); + int skyLight = getWorld().light(LightTypes.SKY, position.x(), position.y(), position.z()); + int groundLight = getWorld().light(LightTypes.BLOCK, position.x(), position.y(), position.z()); + return Math.max(skyLight, groundLight); } @Override - public BiomeType getBiome(BlockVector2 position) { + public BiomeType getBiome(BlockVector3 position) { checkNotNull(position); - return SpongeAdapter.adapt(getWorld().getBiome(position.getBlockX(), 0, position.getBlockZ())); + return BiomeType.REGISTRY.get( + getWorld().registry(RegistryTypes.BIOME) + .valueKey(getWorld().biome(position.x(), position.y(), position.z())) + .asString() + ); } @Override - public boolean setBiome(BlockVector2 position, BiomeType biome) { + public boolean setBiome(BlockVector3 position, BiomeType biome) { checkNotNull(position); checkNotNull(biome); - getWorld().setBiome(position.getBlockX(), 0, position.getBlockZ(), SpongeAdapter.adapt(biome)); + getWorld().setBiome( + position.x(), position.y(), position.z(), + getWorld().registry(RegistryTypes.BIOME).value( + ResourceKey.resolve(biome.id()) + ) + ); return true; } @@ -228,18 +422,32 @@ public void dropItem(Vector3 position, BaseItemStack item) { return; } - org.spongepowered.api.entity.Entity entity = getWorld().createEntity( - EntityTypes.ITEM, - new Vector3d(position.getX(), position.getY(), position.getZ()) + Item itemEntity = getWorld().createEntity( + EntityTypes.ITEM, + new Vector3d(position.x(), position.y(), position.z()) ); - entity.offer(Keys.REPRESENTED_ITEM, SpongeWorldEdit.toSpongeItemStack(item).createSnapshot()); - getWorld().spawnEntity(entity); + itemEntity.item().set( + SpongeAdapter.adapt(item).asImmutable() + ); + getWorld().spawnEntity(itemEntity); } @Override public void simulateBlockMine(BlockVector3 position) { - // TODO + getWorld().destroyBlock( + new Vector3i(position.x(), position.y(), position.z()), + true + ); + } + + @Override + public boolean canPlaceAt(BlockVector3 position, com.sk89q.worldedit.world.block.BlockState blockState) { + return ((net.minecraft.world.level.block.state.BlockState) SpongeAdapter.adapt(blockState)) + .canSurvive( + ((LevelReader) getWorld()), + new BlockPos(position.x(), position.y(), position.z()) + ); } @Override @@ -247,104 +455,117 @@ public int hashCode() { return getWorld().hashCode(); } + @Override + public int getMaxY() { + return getWorld().max().y(); + } + + @Override + public int getMinY() { + return getWorld().min().y(); + } + @Override public boolean equals(Object o) { if (o == null) { return false; - } else if ((o instanceof SpongeWorld)) { - SpongeWorld other = ((SpongeWorld) o); - World otherWorld = other.worldRef.get(); - World thisWorld = worldRef.get(); + } else if ((o instanceof SpongeWorld other)) { + ServerWorld otherWorld = other.worldRef.get(); + ServerWorld thisWorld = worldRef.get(); return otherWorld != null && otherWorld.equals(thisWorld); } else { return o instanceof com.sk89q.worldedit.world.World - && ((com.sk89q.worldedit.world.World) o).getName().equals(getName()); + && ((com.sk89q.worldedit.world.World) o).getName().equals(getName()); } } @Override public List getEntities(Region region) { - List entities = new ArrayList<>(); - for (org.spongepowered.api.entity.Entity entity : getWorld().getEntities()) { - org.spongepowered.api.world.Location loc = entity.getLocation(); - if (region.contains(BlockVector3.at(loc.getX(), loc.getY(), loc.getZ()))) { - entities.add(new SpongeEntity(entity)); - } - } - return entities; + return getWorld() + .entityStream( + SpongeAdapter.adaptVector3i(region.getMinimumPoint()), + SpongeAdapter.adaptVector3i(region.getMaximumPoint()), + // We don't need to force load or clone to copy entities + StreamOptions.builder() + .setCarbonCopy(false) + .setLoadingStyle(StreamOptions.LoadingStyle.NONE) + .build() + ) + .toStream() + .map(ve -> new SpongeEntity(ve.type())) + .collect(Collectors.toList()); } @Override public List getEntities() { - List entities = new ArrayList<>(); - for (org.spongepowered.api.entity.Entity entity : getWorld().getEntities()) { - entities.add(new SpongeEntity(entity)); - } - return entities; + return getWorld().entities().stream() + .map(SpongeEntity::new) + .collect(Collectors.toList()); } - protected abstract void applyEntityData(org.spongepowered.api.entity.Entity entity, BaseEntity data); - @Nullable @Override public Entity createEntity(Location location, BaseEntity entity) { - World world = getWorld(); - - EntityType entityType = Sponge.getRegistry().getType(EntityType.class, entity.getType().getId()).get(); - Vector3d pos = new Vector3d(location.getX(), location.getY(), location.getZ()); - - org.spongepowered.api.entity.Entity newEnt = world.createEntity(entityType, pos); - if (entity.hasNbtData()) { - applyEntityData(newEnt, entity); + Optional> entityType = Sponge.game().registry(RegistryTypes.ENTITY_TYPE) + .findValue(ResourceKey.resolve(entity.getType().id())); + if (entityType.isEmpty()) { + return null; } - - // Overwrite any data set by the NBT application - Vector3 dir = location.getDirection(); - - newEnt.setLocationAndRotation( - new org.spongepowered.api.world.Location<>(getWorld(), pos), - new Vector3d(dir.getX(), dir.getY(), dir.getZ()) - ); - - if (world.spawnEntity(newEnt)) { - return new SpongeEntity(newEnt); + EntityArchetype.Builder builder = EntityArchetype.builder().type(entityType.get()); + var nativeTag = entity.getNbt(); + if (nativeTag != null) { + builder.entityData(NbtAdapter.adaptFromWorldEdit(nativeTag)); } + return builder.build().apply(SpongeAdapter.adapt(location)).map(SpongeEntity::new).orElse(null); + } - return null; + @Override + public void sendBiomeUpdates(Iterable chunks) { + List nativeChunks = chunks instanceof Collection chunkCollection ? Lists.newArrayListWithCapacity(chunkCollection.size()) : Lists.newArrayList(); + for (BlockVector2 chunk : chunks) { + nativeChunks.add(((Level) getWorld()).getChunk(chunk.x(), chunk.z(), ChunkStatus.BIOMES, false)); + } + ((ServerLevel) getWorld()).getChunkSource().chunkMap.resendBiomesForChunks(nativeChunks); } @Override public WeatherType getWeather() { - return WeatherTypes.get(getWorld().getWeather().getId()); + return WeatherTypes.get( + getWorld().weather().type().key(RegistryTypes.WEATHER_TYPE).asString() + ); } @Override public long getRemainingWeatherDuration() { - return getWorld().getRemainingDuration(); + return getWorld().weather().remainingDuration().ticks(); } @Override public void setWeather(WeatherType weatherType) { - getWorld().setWeather(Sponge.getRegistry().getType(Weather.class, weatherType.getId()).get()); + getWorld().setWeather( + Sponge.game().registry(RegistryTypes.WEATHER_TYPE).value( + ResourceKey.resolve(weatherType.id()) + ) + ); } @Override public void setWeather(WeatherType weatherType, long duration) { - getWorld().setWeather(Sponge.getRegistry().getType(Weather.class, weatherType.getId()).get(), duration); + getWorld().setWeather( + Sponge.game().registry(RegistryTypes.WEATHER_TYPE).value( + ResourceKey.resolve(weatherType.id()) + ), + Ticks.of(duration) + ); } @Override public BlockVector3 getSpawnPosition() { - return SpongeAdapter.asBlockVector(getWorld().getSpawnLocation()); + return SpongeAdapter.adaptVector3i(getWorld().properties().spawnPosition()); } - /** - * Thrown when the reference to the world is lost. - */ - private static class WorldReferenceLostException extends WorldEditException { - private WorldReferenceLostException(String message) { - super(message); - } + @Override + public boolean isValid() { + return worldRef.get() != null; } - } diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java index 194ffee159..ecc6506007 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java @@ -19,287 +19,306 @@ package com.sk89q.worldedit.sponge; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME; - +import com.google.common.base.Joiner; import com.google.inject.Inject; -import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.command.util.PermissionCondition; +import com.sk89q.worldedit.event.platform.CommandEvent; +import com.sk89q.worldedit.event.platform.CommandSuggestionEvent; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; -import com.sk89q.worldedit.event.platform.SessionIdleEvent; +import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; +import com.sk89q.worldedit.event.platform.PlatformsRegisteredEvent; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extension.platform.PlatformManager; import com.sk89q.worldedit.internal.anvil.ChunkDeleter; -import com.sk89q.worldedit.sponge.adapter.AdapterLoadException; -import com.sk89q.worldedit.sponge.adapter.SpongeImplAdapter; -import com.sk89q.worldedit.sponge.adapter.SpongeImplLoader; +import com.sk89q.worldedit.internal.command.CommandUtil; +import com.sk89q.worldedit.registry.Registries; import com.sk89q.worldedit.sponge.config.SpongeConfiguration; -import org.bstats.sponge.Metrics2; +import com.sk89q.worldedit.world.biome.BiomeCategory; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.block.BlockCategory; +import com.sk89q.worldedit.world.generation.TreeType; +import com.sk89q.worldedit.world.item.ItemCategory; +import net.kyori.adventure.audience.Audience; import org.apache.logging.log4j.Logger; +import org.bstats.sponge.Metrics; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.Server; import org.spongepowered.api.Sponge; -import org.spongepowered.api.block.BlockSnapshot; import org.spongepowered.api.block.BlockType; -import org.spongepowered.api.block.BlockTypes; -import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.block.entity.BlockEntity; +import org.spongepowered.api.block.entity.CommandBlock; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.command.CommandCause; +import org.spongepowered.api.command.CommandCompletion; +import org.spongepowered.api.command.CommandResult; +import org.spongepowered.api.command.parameter.ArgumentReader; import org.spongepowered.api.config.ConfigDir; -import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.Listener; -import org.spongepowered.api.event.block.InteractBlockEvent; -import org.spongepowered.api.event.filter.cause.Root; -import org.spongepowered.api.event.game.state.GameAboutToStartServerEvent; -import org.spongepowered.api.event.game.state.GameInitializationEvent; -import org.spongepowered.api.event.game.state.GamePostInitializationEvent; -import org.spongepowered.api.event.game.state.GamePreInitializationEvent; -import org.spongepowered.api.event.game.state.GameStartedServerEvent; -import org.spongepowered.api.event.game.state.GameStoppingServerEvent; -import org.spongepowered.api.event.item.inventory.InteractItemEvent; -import org.spongepowered.api.event.network.ClientConnectionEvent; -import org.spongepowered.api.item.ItemType; -import org.spongepowered.api.item.inventory.ItemStack; -import org.spongepowered.api.plugin.Plugin; -import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.event.lifecycle.ConstructPluginEvent; +import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; +import org.spongepowered.api.event.lifecycle.StartedEngineEvent; +import org.spongepowered.api.event.lifecycle.StartingEngineEvent; +import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; +import org.spongepowered.api.registry.RegistryTypes; import org.spongepowered.api.scheduler.Task; -import org.spongepowered.api.world.Location; -import org.spongepowered.api.world.World; +import org.spongepowered.api.world.LocatableBlock; +import org.spongepowered.api.world.generation.feature.FeatureTypes; +import org.spongepowered.api.world.server.ServerWorld; +import org.spongepowered.plugin.PluginContainer; +import org.spongepowered.plugin.builtin.jvm.Plugin; -import java.io.File; -import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME; +import static java.util.stream.Collectors.toList; /** * The Sponge implementation of WorldEdit. */ -@Plugin(id = SpongeWorldEdit.MOD_ID, name = "WorldEdit", - description = "WorldEdit is an easy-to-use in-game world editor for Minecraft", - url = "https://enginehub.org/worldedit/") +@Plugin(SpongeWorldEdit.MOD_ID) public class SpongeWorldEdit { - @Inject - private Logger logger; - - private Metrics2 metrics; - public static final String MOD_ID = "worldedit"; private static final int BSTATS_PLUGIN_ID = 3329; - private SpongePermissionsProvider provider; - - @Inject - private PluginContainer container; - private static SpongeWorldEdit inst; - public static PluginContainer container() { - return inst.container; - } - public static SpongeWorldEdit inst() { return inst; } - private SpongePlatform platform; - private SpongeImplAdapter spongeAdapter; + private final Logger logger; + private final PluginContainer container; + private final SpongeConfiguration config; + private final Path workingDir; - @Inject - private SpongeConfiguration config; - - @Inject @ConfigDir(sharedRoot = false) - private File workingDir; + private SpongePermissionsProvider provider; + private SpongePlatform platform; @Inject - public SpongeWorldEdit(Metrics2.Factory metricsFactory) { + public SpongeWorldEdit(Logger logger, + PluginContainer container, + SpongeConfiguration config, + Metrics.Factory metricsFactory, + @ConfigDir(sharedRoot = false) + Path workingDir) { + this.logger = logger; + this.container = container; + this.config = config; + this.workingDir = workingDir; + metricsFactory.make(BSTATS_PLUGIN_ID); inst = this; - metrics = metricsFactory.make(BSTATS_PLUGIN_ID); } @Listener - public void preInit(GamePreInitializationEvent event) { - // Load configuration - config.load(); + public void onPluginConstruction(ConstructPluginEvent event) { + this.platform = new SpongePlatform(this); - Task.builder().interval(30, TimeUnit.SECONDS).execute(ThreadSafeCache.getInstance()).submit(this); - } + WorldEdit.getInstance().getPlatformManager().register(platform); - @Listener - public void init(GameInitializationEvent event) { - CUIChannelHandler.init(); - } + this.provider = new SpongePermissionsProvider(); + + event.game().eventManager().registerListeners( + container, + new CUIChannelHandler.RegistrationHandler(), + MethodHandles.lookup() + ); + + event.game().eventManager().registerListeners( + container, + new SpongeWorldEditListener(this), + MethodHandles.lookup() + ); - @Listener - public void postInit(GamePostInitializationEvent event) { logger.info("WorldEdit for Sponge (version " + getInternalVersion() + ") is loaded"); } @Listener - public void serverAboutToStart(GameAboutToStartServerEvent event) { - if (this.platform != null) { - logger.warn("GameAboutToStartServerEvent occurred when GameStoppingServerEvent hasn't"); - WorldEdit.getInstance().getPlatformManager().unregister(platform); - } - - final Path delChunks = workingDir.toPath().resolve(DELCHUNKS_FILE_NAME); + public void serverStarting(StartingEngineEvent event) { + final Path delChunks = workingDir.resolve(DELCHUNKS_FILE_NAME); if (Files.exists(delChunks)) { ChunkDeleter.runFromFile(delChunks, true); } + } - this.platform = new SpongePlatform(this); - this.provider = new SpongePermissionsProvider(); - - for (BlockType blockType : Sponge.getRegistry().getAllOf(BlockType.class)) { - // TODO Handle blockstate stuff - String id = blockType.getId(); + @Listener + public void serverStarted(StartedEngineEvent event) { + event.engine().scheduler().submit(Task.builder() + .plugin(container) + .interval(30, TimeUnit.SECONDS) + .execute(ThreadSafeCache.getInstance()) + .build()); + + event.game().registry(RegistryTypes.BLOCK_TYPE).streamEntries().forEach(blockType -> { + String id = blockType.key().asString(); if (!com.sk89q.worldedit.world.block.BlockType.REGISTRY.keySet().contains(id)) { - com.sk89q.worldedit.world.block.BlockType.REGISTRY.register(id, new com.sk89q.worldedit.world.block.BlockType(id)); + com.sk89q.worldedit.world.block.BlockType.REGISTRY.register(id, new com.sk89q.worldedit.world.block.BlockType( + id, + input -> { + BlockType spongeBlockType = Sponge.game().registry(RegistryTypes.BLOCK_TYPE).value( + ResourceKey.resolve(input.getBlockType().id()) + ); + return SpongeAdapter.adapt(spongeBlockType.defaultState()); + } + )); } - } + }); - for (ItemType itemType : Sponge.getRegistry().getAllOf(ItemType.class)) { - String id = itemType.getId(); + event.game().registry(RegistryTypes.ITEM_TYPE).streamEntries().forEach(itemType -> { + String id = itemType.key().asString(); if (!com.sk89q.worldedit.world.item.ItemType.REGISTRY.keySet().contains(id)) { com.sk89q.worldedit.world.item.ItemType.REGISTRY.register(id, new com.sk89q.worldedit.world.item.ItemType(id)); } + }); + + event.game().registry(RegistryTypes.ENTITY_TYPE).streamEntries().forEach(entityType -> { + String id = entityType.key().asString(); + if (!com.sk89q.worldedit.world.entity.EntityType.REGISTRY.keySet().contains(id)) { + com.sk89q.worldedit.world.entity.EntityType.REGISTRY.register(id, new com.sk89q.worldedit.world.entity.EntityType(id)); + } + }); + + for (ServerWorld world : event.engine().worldManager().worlds()) { + world.registry(RegistryTypes.BIOME).streamEntries().forEach(biomeType -> { + String id = biomeType.key().asString(); + if (!BiomeType.REGISTRY.keySet().contains(id)) { + BiomeType.REGISTRY.register(id, new BiomeType(id)); + } + }); } - WorldEdit.getInstance().getPlatformManager().register(platform); + // Disabled until https://github.com/SpongePowered/SpongeAPI/issues/2520 is resolved + // Will also need implementations in SpongeWorld to do placement + // Sponge.server().registry(RegistryTypes.FEATURE).streamEntries().forEach(feature -> { + // String id = feature.key().asString(); + // if (!ConfiguredFeatureType.REGISTRY.keySet().contains(id)) { + // ConfiguredFeatureType.REGISTRY.register(id, new ConfiguredFeatureType(id)); + // } + // }); + // Sponge.server().registry(RegistryTypes.STRUCTURE).streamEntries().forEach(structure -> { + // String id = structure.key().asString(); + // if (!StructureType.REGISTRY.keySet().contains(id)) { + // StructureType.REGISTRY.register(id, new StructureType(id)); + // } + // }); + + event.game().registry(RegistryTypes.PLACED_FEATURE).streamEntries().forEach(feature -> { + String id = feature.key().asString(); + var underlyingFeatureType = feature.value().feature().type(); + if (underlyingFeatureType.equals(FeatureTypes.TREE) || underlyingFeatureType.equals(FeatureTypes.FALLEN_TREE) || underlyingFeatureType.equals(FeatureTypes.CORAL_TREE)) { + if (!TreeType.REGISTRY.keySet().contains(id)) { + TreeType.REGISTRY.register(id, new TreeType(id)); + } + } + }); + event.game().registry(RegistryTypes.BLOCK_TYPE).tags().forEach(blockTypeTag -> { + String id = blockTypeTag.key().asString(); + if (!BlockCategory.REGISTRY.keySet().contains(id)) { + BlockCategory.REGISTRY.register(id, new BlockCategory(id)); + } + }); + event.game().registry(RegistryTypes.ITEM_TYPE).tags().forEach(itemTypeTag -> { + String id = itemTypeTag.key().asString(); + if (!ItemCategory.REGISTRY.keySet().contains(id)) { + ItemCategory.REGISTRY.register(id, new ItemCategory(id)); + } + }); + event.game().registry(RegistryTypes.BIOME).tags().forEach(biomeTag -> { + String id = biomeTag.key().asString(); + if (!BiomeCategory.REGISTRY.keySet().contains(id)) { + BiomeCategory.REGISTRY.register(id, new BiomeCategory(id, () -> event.game().registry(RegistryTypes.BIOME).taggedValues(biomeTag).map(SpongeAdapter::adapt).collect(Collectors.toSet()))); + } + }); + + Registries.get(""); + + config.load(); + WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform)); } @Listener - public void serverStopping(GameStoppingServerEvent event) { + public void serverStopping(StoppingEngineEvent event) { WorldEdit worldEdit = WorldEdit.getInstance(); worldEdit.getSessionManager().unload(); - worldEdit.getPlatformManager().unregister(platform); + WorldEdit.getInstance().getEventBus().post(new PlatformUnreadyEvent(platform)); } @Listener - public void serverStarted(GameStartedServerEvent event) { - WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent()); - - loadAdapter(); - } - - private void loadAdapter() { - WorldEdit worldEdit = WorldEdit.getInstance(); - - // Attempt to load a Sponge adapter - SpongeImplLoader adapterLoader = new SpongeImplLoader(); - - try { - adapterLoader.addFromPath(getClass().getClassLoader()); - } catch (IOException e) { - logger.warn("Failed to search path for Sponge adapters"); + public void registerCommand(RegisterCommandEvent event) { + WorldEdit.getInstance().getEventBus().post(new PlatformsRegisteredEvent()); + PlatformManager manager = WorldEdit.getInstance().getPlatformManager(); + Platform commandsPlatform = manager.queryCapability(Capability.USER_COMMANDS); + if (commandsPlatform != platform || !platform.isHookingEvents()) { + // We're not in control of commands/events -- do not register. + return; } - try { - adapterLoader.addFromJar(container.getSource().get().toFile()); - } catch (IOException e) { - logger.warn("Failed to search " + container.getSource().get().toFile() + " for Sponge adapters", e); - } - try { - spongeAdapter = adapterLoader.loadAdapter(); - logger.info("Using " + spongeAdapter.getClass().getCanonicalName() + " as the Sponge adapter"); - } catch (AdapterLoadException e) { - Platform platform = worldEdit.getPlatformManager().queryCapability(Capability.WORLD_EDITING); - if (platform instanceof SpongePlatform) { - logger.warn(e.getMessage()); - } else { - logger.info("WorldEdit could not find a Sponge adapter for this MC version, " + - "but it seems that you have another implementation of WorldEdit installed (" + platform.getPlatformName() + ") " + - "that handles the world editing."); + List commands = manager.getPlatformCommandManager().getCommandManager() + .getAllCommands().toList(); + for (org.enginehub.piston.Command command : commands) { + registerAdaptedCommand(event, command); + + Set perms = command.getCondition().as(PermissionCondition.class) + .map(PermissionCondition::getPermissions) + .orElseGet(Collections::emptySet); + if (!perms.isEmpty()) { + perms.forEach(getPermissionsProvider()::registerPermission); } } } - public SpongeImplAdapter getAdapter() { - return this.spongeAdapter; - } - - @Listener - public void onPlayerItemInteract(InteractItemEvent.Secondary event, @Root Player spongePlayer) { - if (platform == null) { - return; + private String rebuildArguments(String commandLabel, String args) { + int plSep = commandLabel.indexOf(':'); + if (plSep >= 0 && plSep < commandLabel.length() + 1) { + commandLabel = commandLabel.substring(plSep + 1); } - if (!platform.isHookingEvents()) return; // We have to be told to catch these events - - WorldEdit we = WorldEdit.getInstance(); + StringBuilder sb = new StringBuilder("/").append(commandLabel); - SpongePlayer player = wrapPlayer(spongePlayer); - if (we.handleRightClick(player)) { - event.setCancelled(true); + String[] split = args.split(" ", -1); + if (split.length > 0) { + sb.append(" "); } + return Joiner.on(" ").appendTo(sb, split).toString(); } - @Listener - public void onPlayerInteract(InteractBlockEvent event, @Root Player spongePlayer) { - if (platform == null) { - return; - } - - if (!platform.isHookingEvents()) return; // We have to be told to catch these events - - WorldEdit we = WorldEdit.getInstance(); - - SpongePlayer player = wrapPlayer(spongePlayer); - com.sk89q.worldedit.world.World world = player.getWorld(); - - BlockSnapshot targetBlock = event.getTargetBlock(); - Optional> optLoc = targetBlock.getLocation(); - - BlockType interactedType = targetBlock.getState().getType(); - if (event instanceof InteractBlockEvent.Primary) { - if (interactedType != BlockTypes.AIR) { - if (!optLoc.isPresent()) { - return; - } - - Location loc = optLoc.get(); - com.sk89q.worldedit.util.Location pos = new com.sk89q.worldedit.util.Location( - world, loc.getX(), loc.getY(), loc.getZ()); - - if (we.handleBlockLeftClick(player, pos)) { - event.setCancelled(true); - } - - if (we.handleArmSwing(player)) { - event.setCancelled(true); - } - } else { - if (we.handleArmSwing(player)) { - event.setCancelled(true); - } - } - } else if (event instanceof InteractBlockEvent.Secondary) { - if (!optLoc.isPresent()) { - return; - } - - Location loc = optLoc.get(); - com.sk89q.worldedit.util.Location pos = new com.sk89q.worldedit.util.Location( - world, loc.getX(), loc.getY(), loc.getZ()); - - if (we.handleBlockRightClick(player, pos)) { - event.setCancelled(true); + private void registerAdaptedCommand(RegisterCommandEvent event, org.enginehub.piston.Command command) { + CommandAdapter adapter = new CommandAdapter(command) { + @Override + public CommandResult process(CommandCause cause, ArgumentReader.Mutable arguments) { + CommandEvent weEvent = new CommandEvent(SpongeWorldEdit.inst().wrapCommandCause(cause), rebuildArguments(command.getName(), arguments.remaining()).trim()); + WorldEdit.getInstance().getEventBus().post(weEvent); + return weEvent.isCancelled() ? CommandResult.success() : CommandResult.builder().build(); } - if (we.handleRightClick(player)) { - event.setCancelled(true); + @Override + public List complete(CommandCause cause, ArgumentReader.Mutable arguments) { + String args = rebuildArguments(command.getName(), arguments.remaining()); + CommandSuggestionEvent weEvent = new CommandSuggestionEvent(SpongeWorldEdit.inst().wrapCommandCause(cause), args); + WorldEdit.getInstance().getEventBus().post(weEvent); + return CommandUtil.fixSuggestions(args, weEvent.getSuggestions()) + .stream().map(CommandCompletion::of).collect(toList()); } - } + }; + event.register( + container, adapter, command.getName(), command.getAliases().toArray(new String[0]) + ); } - @Listener - public void onPlayerQuit(ClientConnectionEvent.Disconnect event) { - WorldEdit.getInstance().getEventBus() - .post(new SessionIdleEvent(new SpongePlayer.SessionKeyImpl(event.getTargetEntity()))); - } - - public static ItemStack toSpongeItemStack(BaseItemStack item) { - return inst().getAdapter().makeSpongeStack(item); + public PluginContainer getPluginContainer() { + return container; } /** @@ -311,46 +330,27 @@ SpongeConfiguration getConfig() { return this.config; } - /** - * Get the WorldEdit proxy for the given player. - * - * @param player the player - * @return the WorldEdit player - */ - public SpongePlayer wrapPlayer(Player player) { - checkNotNull(player); - return new SpongePlayer(platform, player); - } - - public Actor wrapCommandSource(CommandSource sender) { - if (sender instanceof Player) { - return wrapPlayer((Player) sender); + public Actor wrapCommandCause(CommandCause cause) { + Object rootCause = cause.root(); + if (rootCause instanceof ServerPlayer serverPlayer) { + return SpongeAdapter.adapt(serverPlayer); + } + if (rootCause instanceof LocatableBlock locatableBlock) { + Optional optionalBlockEntity = locatableBlock.world().blockEntity(locatableBlock.blockPosition()); + if (optionalBlockEntity.isPresent()) { + BlockEntity blockEntity = optionalBlockEntity.get(); + if (blockEntity instanceof CommandBlock commandBlock) { + return new SpongeBlockCommandSender(this, commandBlock); + } + } + } + if (rootCause instanceof Audience audience) { + return new SpongeCommandSender(audience); } - return new SpongeCommandSender(this, sender); - } - - /** - * Get the session for a player. - * - * @param player the player - * @return the session - */ - public LocalSession getSession(Player player) { - checkNotNull(player); - return WorldEdit.getInstance().getSessionManager().get(wrapPlayer(player)); + throw new UnsupportedOperationException("Cannot wrap " + rootCause.getClass()); } - /** - * Get the WorldEdit proxy for the given world. - * - * @param world the world - * @return the WorldEdit world - */ - public SpongeWorld getWorld(World world) { - checkNotNull(world); - return getAdapter().getWorld(world); - } /** * Get the WorldEdit proxy for the platform. @@ -361,12 +361,16 @@ public Platform getPlatform() { return this.platform; } + SpongePlatform getInternalPlatform() { + return this.platform; + } + /** * Get the working directory where WorldEdit's files are stored. * * @return the working directory */ - public File getWorkingDir() { + public Path getWorkingDir() { return this.workingDir; } @@ -376,7 +380,7 @@ public File getWorkingDir() { * @return a version string */ String getInternalVersion() { - return container.getVersion().orElse("Unknown"); + return container.metadata().version().toString(); } public void setPermissionsProvider(SpongePermissionsProvider provider) { @@ -386,5 +390,4 @@ public void setPermissionsProvider(SpongePermissionsProvider provider) { public SpongePermissionsProvider getPermissionsProvider() { return provider; } - }