From 7daead4aecda36da29c6fa759e9e3727dcab087d Mon Sep 17 00:00:00 2001 From: Octavia Togami Date: Sun, 8 Dec 2024 13:09:01 -0800 Subject: [PATCH] Fix block entities in features/structures Fixes #2669 --- .../impl/v1_21_4/PaperweightAdapter.java | 56 +++++++---- .../PaperweightServerLevelDelegateProxy.java | 95 ++++++++++++++----- .../sk89q/worldedit/fabric/FabricAdapter.java | 8 +- .../sk89q/worldedit/fabric/FabricWorld.java | 68 ++++++++----- .../FabricServerLevelDelegateProxy.java | 92 +++++++++++++----- .../worldedit/neoforge/NeoForgeWorld.java | 69 +++++++++----- .../NeoForgeServerLevelDelegateProxy.java | 92 +++++++++++++----- 7 files changed, 342 insertions(+), 138 deletions(-) diff --git a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java index 2cc50bd0d0..ddd0684942 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/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; @@ -117,7 +118,6 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelSettings; -import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; @@ -939,33 +939,55 @@ public void initializeRegistries() { public boolean generateFeature(ConfiguredFeatureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); - ConfiguredFeature k = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ConfiguredFeature feature = originalWorld.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); 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())); + 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())); + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); + } } public boolean generateStructure(StructureType type, World world, EditSession session, BlockVector3 pt) { ServerLevel originalWorld = ((CraftWorld) world).getHandle(); Registry structureRegistry = originalWorld.registryAccess().lookupOrThrow(Registries.STRUCTURE); - Structure k = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); - if (k == null) { + Structure structure = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); + if (structure == null) { return false; } ServerChunkCache chunkManager = originalWorld.getChunkSource(); - WorldGenLevel proxyLevel = PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this); - ChunkPos chunkPos = new ChunkPos(new BlockPos(pt.x(), pt.y(), pt.z())); - StructureStart structureStart = k.generate(structureRegistry.wrapAsHolder(k), originalWorld.dimension(), originalWorld.registryAccess(), chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), originalWorld.getStructureManager(), originalWorld.getSeed(), chunkPos, 0, proxyLevel, biome -> true); + try (PaperweightServerLevelDelegateProxy.LevelAndProxy proxyLevel = + PaperweightServerLevelDelegateProxy.newInstance(session, originalWorld, this)) { + ChunkPos chunkPos = new ChunkPos(new BlockPos(pt.x(), pt.y(), pt.z())); + StructureStart structureStart = structure.generate( + structureRegistry.wrapAsHolder(structure), originalWorld.dimension(), originalWorld.registryAccess(), + chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), + originalWorld.getStructureManager(), originalWorld.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, originalWorld.structureManager(), chunkManager.getGenerator(), originalWorld.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), originalWorld.getMinY(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), originalWorld.getMaxY(), chunkPosx.getMaxBlockZ()), chunkPosx)); - return 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(), originalWorld.structureManager(), chunkManager.getGenerator(), + originalWorld.getRandom(), + new BoundingBox( + chunkPosx.getMinBlockX(), originalWorld.getMinY(), chunkPosx.getMinBlockZ(), + chunkPosx.getMaxBlockX(), originalWorld.getMaxY(), chunkPosx.getMaxBlockZ() + ), chunkPosx + ) + ); + return true; + } + } catch (MaxChangedBlocksException e) { + throw new RuntimeException(e); } } diff --git a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightServerLevelDelegateProxy.java b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightServerLevelDelegateProxy.java index aab88beeb0..e3dc39b3f5 100644 --- a/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightServerLevelDelegateProxy.java +++ b/worldedit-bukkit/adapters/adapter-1.21.4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_4/PaperweightServerLevelDelegateProxy.java @@ -28,7 +28,6 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.concurrency.LazyReference; -import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.entity.EntityTypes; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; @@ -37,6 +36,8 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; @@ -50,12 +51,19 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; -public class PaperweightServerLevelDelegateProxy implements InvocationHandler { +public class PaperweightServerLevelDelegateProxy implements InvocationHandler, AutoCloseable { + + private static BlockVector3 adapt(BlockPos blockPos) { + return BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } private final EditSession editSession; private final ServerLevel serverLevel; private final PaperweightAdapter adapter; + private final Map createdBlockEntities = new HashMap<>(); private PaperweightServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) { this.editSession = editSession; @@ -63,46 +71,71 @@ private PaperweightServerLevelDelegateProxy(EditSession editSession, ServerLevel this.adapter = adapter; } - public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) { - return (WorldGenLevel) Proxy.newProxyInstance( - serverLevel.getClass().getClassLoader(), - serverLevel.getClass().getInterfaces(), - new PaperweightServerLevelDelegateProxy(editSession, serverLevel, adapter) + public record LevelAndProxy(WorldGenLevel level, PaperweightServerLevelDelegateProxy proxy) implements AutoCloseable { + @Override + public void close() throws MaxChangedBlocksException { + proxy.close(); + } + } + + public static LevelAndProxy newInstance(EditSession editSession, ServerLevel serverLevel, PaperweightAdapter adapter) { + PaperweightServerLevelDelegateProxy proxy = new PaperweightServerLevelDelegateProxy(editSession, serverLevel, adapter); + return new LevelAndProxy( + (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + proxy + ), + proxy ); } @Nullable private BlockEntity getBlockEntity(BlockPos blockPos) { - BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); - if (tileEntity == null) { - return null; - } - tileEntity.loadWithComponents( - (CompoundTag) adapter.fromNative(this.editSession.getFullBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ())).getNbtReference().getValue()), - this.serverLevel.registryAccess() - ); - - return tileEntity; + // This doesn't synthesize or load from world. I think editing existing block entities without setting the block + // (in the context of features) should not be supported in the first place. + BlockVector3 pos = adapt(blockPos); + return createdBlockEntities.get(pos); } private BlockState getBlockState(BlockPos blockPos) { - return adapter.adapt(this.editSession.getBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()))); + return adapter.adapt(this.editSession.getBlock(adapt(blockPos))); } private boolean setBlock(BlockPos blockPos, BlockState blockState) { try { - return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), adapter.adapt(blockState)); + handleBlockEntity(blockPos, blockState); + return editSession.setBlock(adapt(blockPos), adapter.adapt(blockState)); } catch (MaxChangedBlocksException e) { throw new RuntimeException(e); } } - private boolean removeBlock(BlockPos blockPos) { - try { - return editSession.setBlock(BlockVector3.at(blockPos.getX(), blockPos.getY(), blockPos.getZ()), BlockTypes.AIR.getDefaultState()); - } catch (MaxChangedBlocksException e) { - throw new RuntimeException(e); + // For BlockEntity#setBlockState, not sure why it's deprecated + @SuppressWarnings("deprecation") + private void handleBlockEntity(BlockPos blockPos, BlockState blockState) { + BlockVector3 pos = adapt(blockPos); + if (blockState.hasBlockEntity()) { + if (!(blockState.getBlock() instanceof EntityBlock entityBlock)) { + // This will probably never happen, as Mojang's own code assumes that + // hasBlockEntity implies instanceof EntityBlock, but just to be safe... + throw new AssertionError("BlockState has block entity but block is not an EntityBlock: " + blockState); + } + BlockEntity newEntity = entityBlock.newBlockEntity(blockPos, blockState); + if (newEntity != null) { + newEntity.setBlockState(blockState); + createdBlockEntities.put(pos, newEntity); + // Should we load existing NBT here? This is for feature / structure gen so it seems unnecessary. + // But it would align with the behavior of the real setBlock method. + return; + } } + // Discard any block entity that was previously created if new block is set without block entity + createdBlockEntities.remove(pos); + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + return setBlock(blockPos, Blocks.AIR.defaultBlockState()); } private boolean addEntity(Entity entity) { @@ -117,6 +150,20 @@ private boolean addEntity(Entity entity) { return editSession.createEntity(location, baseEntity) != null; } + @Override + public void close() throws MaxChangedBlocksException { + for (Map.Entry entry : createdBlockEntities.entrySet()) { + BlockVector3 blockPos = entry.getKey(); + BlockEntity blockEntity = entry.getValue(); + net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverLevel.registryAccess()); + editSession.setBlock( + blockPos, + adapter.adapt(blockEntity.getBlockState()) + .toBaseBlock(LazyReference.from(() -> (LinCompoundTag) adapter.toNative(tag))) + ); + } + } + private static void addMethodHandleToTable( ImmutableTable.Builder table, String methodName, diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java index 493aa7f35a..e5c2e7882b 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricAdapter.java @@ -41,6 +41,7 @@ import com.sk89q.worldedit.world.item.ItemTypes; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; @@ -195,13 +196,18 @@ public static BaseBlock adapt(BlockEntity blockEntity) { if (!blockEntity.hasLevel()) { throw new IllegalArgumentException("BlockEntity must have a level"); } + RegistryAccess registries = blockEntity.getLevel().registryAccess(); + return adapt(blockEntity, registries); + } + + public static BaseBlock adapt(BlockEntity blockEntity, RegistryAccess registries) { int blockStateId = Block.getId(blockEntity.getBlockState()); BlockState worldEdit = BlockStateIdAccess.getBlockStateById(blockStateId); if (worldEdit == null) { worldEdit = FabricTransmogrifier.transmogToWorldEdit(blockEntity.getBlockState()); } // Save this outside the reference to ensure it doesn't mutate - CompoundTag savedNative = blockEntity.saveWithId(blockEntity.getLevel().registryAccess()); + CompoundTag savedNative = blockEntity.saveWithId(registries); return worldEdit.toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(savedNative))); } 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 4049376528..c255cc18a7 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 @@ -28,6 +28,7 @@ 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; @@ -88,7 +89,6 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; -import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -471,7 +471,7 @@ private List> submitChunkLoadTasks(Region region, } @Override - public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) { + public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { ServerLevel world = (ServerLevel) getWorld(); ConfiguredFeature generator = Optional.ofNullable(createTreeFeatureGenerator(type)) .map(k -> world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(k)) @@ -480,43 +480,65 @@ public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 if (type == TreeType.CHORUS_PLANT) { position = position.add(0, 1, 0); } - WorldGenLevel proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world); - return generator != null && generator.place( - proxyLevel, chunkManager.getGenerator(), random, - FabricAdapter.toBlockPos(position) - ); + try (FabricServerLevelDelegateProxy.LevelAndProxy proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + proxyLevel.level(), chunkManager.getGenerator(), random, + FabricAdapter.toBlockPos(position) + ); + } } public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { ServerLevel world = (ServerLevel) getWorld(); - ConfiguredFeature k = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ConfiguredFeature feature = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); ServerChunkCache chunkManager = world.getChunkSource(); - WorldGenLevel proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world); - return k != null && k.place(proxyLevel, chunkManager.getGenerator(), random, FabricAdapter.toBlockPos(position)); + 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 k = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); - if (k == null) { + Structure structure = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); + if (structure == null) { return false; } ServerChunkCache chunkManager = world.getChunkSource(); - WorldGenLevel proxyLevel = FabricServerLevelDelegateProxy.newInstance(editSession, world); - ChunkPos chunkPos = new ChunkPos(new BlockPos(position.x(), position.y(), position.z())); - StructureStart structureStart = k.generate(structureRegistry.wrapAsHolder(k), world.dimension(), world.registryAccess(), chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), world.getStructureManager(), world.getSeed(), chunkPos, 0, proxyLevel, biome -> true); + 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, world.structureManager(), chunkManager.getGenerator(), world.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), world.getMinY(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), world.getMaxY(), chunkPosx.getMaxBlockZ()), chunkPosx)); - return 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); } } diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java index 9263078d74..e3bdb96309 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/internal/FabricServerLevelDelegateProxy.java @@ -23,13 +23,15 @@ import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.fabric.FabricAdapter; +import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.util.Location; -import com.sk89q.worldedit.world.block.BlockTypes; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; @@ -37,40 +39,45 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; -public class FabricServerLevelDelegateProxy implements InvocationHandler { +public class FabricServerLevelDelegateProxy implements InvocationHandler, AutoCloseable { private final EditSession editSession; private final ServerLevel serverLevel; + private final Map createdBlockEntities = new HashMap<>(); private FabricServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel) { this.editSession = editSession; this.serverLevel = serverLevel; } - public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel) { - return (WorldGenLevel) Proxy.newProxyInstance( - serverLevel.getClass().getClassLoader(), - serverLevel.getClass().getInterfaces(), - new FabricServerLevelDelegateProxy(editSession, serverLevel) - ); + public record LevelAndProxy(WorldGenLevel level, FabricServerLevelDelegateProxy proxy) implements AutoCloseable { + @Override + public void close() throws MaxChangedBlocksException { + proxy.close(); + } } - @Nullable - private BlockEntity getBlockEntity(BlockPos blockPos) { - BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); - if (tileEntity == null) { - return null; - } - BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos)); - newEntity.loadWithComponents( - NBTConverter.toNative( - this.editSession.getFullBlock(FabricAdapter.adapt(blockPos)).getNbtReference().getValue() + public static LevelAndProxy newInstance(EditSession editSession, ServerLevel serverLevel) { + FabricServerLevelDelegateProxy proxy = new FabricServerLevelDelegateProxy(editSession, serverLevel); + return new LevelAndProxy( + (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + proxy ), - this.serverLevel.registryAccess() + proxy ); + } - return newEntity; + @Nullable + private BlockEntity getBlockEntity(BlockPos blockPos) { + // This doesn't synthesize or load from world. I think editing existing block entities without setting the block + // (in the context of features) should not be supported in the first place. + BlockVector3 pos = FabricAdapter.adapt(blockPos); + return createdBlockEntities.get(pos); } private BlockState getBlockState(BlockPos blockPos) { @@ -79,18 +86,38 @@ private BlockState getBlockState(BlockPos blockPos) { private boolean setBlock(BlockPos blockPos, BlockState blockState) { try { + handleBlockEntity(blockPos, blockState); return editSession.setBlock(FabricAdapter.adapt(blockPos), FabricAdapter.adapt(blockState)); } catch (MaxChangedBlocksException e) { throw new RuntimeException(e); } } - private boolean removeBlock(BlockPos blockPos, boolean bl) { - try { - return editSession.setBlock(FabricAdapter.adapt(blockPos), BlockTypes.AIR.getDefaultState()); - } catch (MaxChangedBlocksException e) { - throw new RuntimeException(e); + // For BlockEntity#setBlockState, not sure why it's deprecated + @SuppressWarnings("deprecation") + private void handleBlockEntity(BlockPos blockPos, BlockState blockState) { + BlockVector3 pos = FabricAdapter.adapt(blockPos); + if (blockState.hasBlockEntity()) { + if (!(blockState.getBlock() instanceof EntityBlock entityBlock)) { + // This will probably never happen, as Mojang's own code assumes that + // hasBlockEntity implies instanceof EntityBlock, but just to be safe... + throw new AssertionError("BlockState has block entity but block is not an EntityBlock: " + blockState); + } + BlockEntity newEntity = entityBlock.newBlockEntity(blockPos, blockState); + if (newEntity != null) { + newEntity.setBlockState(blockState); + createdBlockEntities.put(pos, newEntity); + // Should we load existing NBT here? This is for feature / structure gen so it seems unnecessary. + // But it would align with the behavior of the real setBlock method. + return; + } } + // Discard any block entity that was previously created if new block is set without block entity + createdBlockEntities.remove(pos); + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + return setBlock(blockPos, Blocks.AIR.defaultBlockState()); } private boolean addEntity(Entity entity) { @@ -100,6 +127,18 @@ private boolean addEntity(Entity entity) { return editSession.createEntity(location, baseEntity) != null; } + @Override + public void close() throws MaxChangedBlocksException { + for (Map.Entry entry : createdBlockEntities.entrySet()) { + BlockVector3 blockPos = entry.getKey(); + BlockEntity blockEntity = entry.getValue(); + editSession.setBlock( + blockPos, + FabricAdapter.adapt(blockEntity, serverLevel.registryAccess()) + ); + } + } + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { @@ -128,7 +167,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return addEntity(entity); } } - default -> { } + default -> { + } } return method.invoke(this.serverLevel, args); 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 index c16ea301e3..4b529ce17b 100644 --- a/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorld.java +++ b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/NeoForgeWorld.java @@ -28,6 +28,7 @@ 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; @@ -87,7 +88,6 @@ import net.minecraft.world.item.context.UseOnContext; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; -import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -452,7 +452,7 @@ private List> submitChunkLoadTasks(Region region, } @Override - public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) { + public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 position) throws MaxChangedBlocksException { ServerLevel world = getWorld(); ConfiguredFeature generator = Optional.ofNullable(createTreeFeatureGenerator(type)) .flatMap(k -> world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getOptional(k)) @@ -461,42 +461,67 @@ public boolean generateTree(TreeType type, EditSession editSession, BlockVector3 if (type == TreeType.CHORUS_PLANT) { position = position.add(0, 1, 0); } - WorldGenLevel levelProxy = NeoForgeServerLevelDelegateProxy.newInstance(editSession, world); - return generator != null && generator.place( - levelProxy, chunkManager.getGenerator(), random, NeoForgeAdapter.toBlockPos(position) - ); + try (NeoForgeServerLevelDelegateProxy.LevelAndProxy levelProxy = + NeoForgeServerLevelDelegateProxy.newInstance(editSession, world)) { + return generator != null && generator.place( + levelProxy.level(), chunkManager.getGenerator(), random, NeoForgeAdapter.toBlockPos(position) + ); + } } + @Override public boolean generateFeature(ConfiguredFeatureType type, EditSession editSession, BlockVector3 position) { ServerLevel world = getWorld(); - ConfiguredFeature k = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); + ConfiguredFeature feature = world.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).getValue(ResourceLocation.tryParse(type.id())); ServerChunkCache chunkManager = world.getChunkSource(); - WorldGenLevel levelProxy = NeoForgeServerLevelDelegateProxy.newInstance(editSession, world); - return k != null && k.place(levelProxy, chunkManager.getGenerator(), random, NeoForgeAdapter.toBlockPos(position)); + 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 k = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); - if (k == null) { + Structure structure = structureRegistry.getValue(ResourceLocation.tryParse(type.id())); + if (structure == null) { return false; } ServerChunkCache chunkManager = world.getChunkSource(); - WorldGenLevel proxyLevel = NeoForgeServerLevelDelegateProxy.newInstance(editSession, world); - ChunkPos chunkPos = new ChunkPos(new BlockPos(position.x(), position.y(), position.z())); - StructureStart structureStart = k.generate(structureRegistry.wrapAsHolder(k), world.dimension(), world.registryAccess(), chunkManager.getGenerator(), chunkManager.getGenerator().getBiomeSource(), chunkManager.randomState(), world.getStructureManager(), world.getSeed(), chunkPos, 0, proxyLevel, biome -> true); + 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(proxyLevel, world.structureManager(), chunkManager.getGenerator(), world.getRandom(), new BoundingBox(chunkPosx.getMinBlockX(), world.getMinY(), chunkPosx.getMinBlockZ(), chunkPosx.getMaxBlockX(), world.getMaxY(), chunkPosx.getMaxBlockZ()), chunkPosx)); - return 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); } } diff --git a/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/internal/NeoForgeServerLevelDelegateProxy.java b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/internal/NeoForgeServerLevelDelegateProxy.java index 64266c1663..b6abff1fed 100644 --- a/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/internal/NeoForgeServerLevelDelegateProxy.java +++ b/worldedit-neoforge/src/main/java/com/sk89q/worldedit/neoforge/internal/NeoForgeServerLevelDelegateProxy.java @@ -22,14 +22,17 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.neoforge.NeoForgeAdapter; import com.sk89q.worldedit.util.Location; -import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.util.concurrency.LazyReference; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; @@ -37,40 +40,45 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; -public class NeoForgeServerLevelDelegateProxy implements InvocationHandler { +public class NeoForgeServerLevelDelegateProxy implements InvocationHandler, AutoCloseable { private final EditSession editSession; private final ServerLevel serverLevel; + private final Map createdBlockEntities = new HashMap<>(); private NeoForgeServerLevelDelegateProxy(EditSession editSession, ServerLevel serverLevel) { this.editSession = editSession; this.serverLevel = serverLevel; } - public static WorldGenLevel newInstance(EditSession editSession, ServerLevel serverLevel) { - return (WorldGenLevel) Proxy.newProxyInstance( - serverLevel.getClass().getClassLoader(), - serverLevel.getClass().getInterfaces(), - new NeoForgeServerLevelDelegateProxy(editSession, serverLevel) - ); + public record LevelAndProxy(WorldGenLevel level, NeoForgeServerLevelDelegateProxy proxy) implements AutoCloseable { + @Override + public void close() throws MaxChangedBlocksException { + proxy.close(); + } } - @Nullable - private BlockEntity getBlockEntity(BlockPos blockPos) { - BlockEntity tileEntity = this.serverLevel.getChunkAt(blockPos).getBlockEntity(blockPos); - if (tileEntity == null) { - return null; - } - BlockEntity newEntity = tileEntity.getType().create(blockPos, getBlockState(blockPos)); - newEntity.loadWithComponents( - NBTConverter.toNative( - this.editSession.getFullBlock(NeoForgeAdapter.adapt(blockPos)).getNbtReference().getValue() + public static LevelAndProxy newInstance(EditSession editSession, ServerLevel serverLevel) { + NeoForgeServerLevelDelegateProxy proxy = new NeoForgeServerLevelDelegateProxy(editSession, serverLevel); + return new LevelAndProxy( + (WorldGenLevel) Proxy.newProxyInstance( + serverLevel.getClass().getClassLoader(), + serverLevel.getClass().getInterfaces(), + proxy ), - this.serverLevel.registryAccess() + proxy ); + } - return newEntity; + @Nullable + private BlockEntity getBlockEntity(BlockPos blockPos) { + // This doesn't synthesize or load from world. I think editing existing block entities without setting the block + // (in the context of features) should not be supported in the first place. + BlockVector3 pos = NeoForgeAdapter.adapt(blockPos); + return createdBlockEntities.get(pos); } private BlockState getBlockState(BlockPos blockPos) { @@ -79,18 +87,38 @@ private BlockState getBlockState(BlockPos blockPos) { private boolean setBlock(BlockPos blockPos, BlockState blockState) { try { + handleBlockEntity(blockPos, blockState); return editSession.setBlock(NeoForgeAdapter.adapt(blockPos), NeoForgeAdapter.adapt(blockState)); } catch (MaxChangedBlocksException e) { throw new RuntimeException(e); } } - private boolean removeBlock(BlockPos blockPos, boolean bl) { - try { - return editSession.setBlock(NeoForgeAdapter.adapt(blockPos), BlockTypes.AIR.getDefaultState()); - } catch (MaxChangedBlocksException e) { - throw new RuntimeException(e); + // For BlockEntity#setBlockState, not sure why it's deprecated + @SuppressWarnings("deprecation") + private void handleBlockEntity(BlockPos blockPos, BlockState blockState) { + BlockVector3 pos = NeoForgeAdapter.adapt(blockPos); + if (blockState.hasBlockEntity()) { + if (!(blockState.getBlock() instanceof EntityBlock entityBlock)) { + // This will probably never happen, as Mojang's own code assumes that + // hasBlockEntity implies instanceof EntityBlock, but just to be safe... + throw new AssertionError("BlockState has block entity but block is not an EntityBlock: " + blockState); + } + BlockEntity newEntity = entityBlock.newBlockEntity(blockPos, blockState); + if (newEntity != null) { + newEntity.setBlockState(blockState); + createdBlockEntities.put(pos, newEntity); + // Should we load existing NBT here? This is for feature / structure gen so it seems unnecessary. + // But it would align with the behavior of the real setBlock method. + return; + } } + // Discard any block entity that was previously created if new block is set without block entity + createdBlockEntities.remove(pos); + } + + private boolean removeBlock(BlockPos blockPos, boolean bl) { + return setBlock(blockPos, Blocks.AIR.defaultBlockState()); } private boolean addEntity(Entity entity) { @@ -100,6 +128,20 @@ private boolean addEntity(Entity entity) { return editSession.createEntity(location, baseEntity) != null; } + @Override + public void close() throws MaxChangedBlocksException { + for (Map.Entry entry : createdBlockEntities.entrySet()) { + BlockVector3 blockPos = entry.getKey(); + BlockEntity blockEntity = entry.getValue(); + net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverLevel.registryAccess()); + editSession.setBlock( + blockPos, + NeoForgeAdapter.adapt(blockEntity.getBlockState()) + .toBaseBlock(LazyReference.from(() -> NBTConverter.fromNative(tag))) + ); + } + } + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) {