support block state mutation of already placed block entity in BlockStateListPopulator

As seen in https://github.com/PaperMC/Paper/issues/12645, the BlockStateListPopulator always destroy
block entity data when the block state change. However this is something that should be supported
so plugin can retrieve block entity data of captured blocks.
Additionally only create snapshots at the end of the capture so we don't need to refresh block entity
data for decorator like the beehive, this is possible since multiple capture
at the same position is not supported and will overwrite the previous data anyway.
This commit is contained in:
Lulu13022002
2025-06-08 19:55:19 +02:00
parent 48e94e8c89
commit c5fc0dc84b
12 changed files with 75 additions and 78 deletions

View File

@@ -32,12 +32,12 @@ public class SpongeAbsorbEvent extends BlockEvent implements Cancellable {
}
/**
* Get a list of all blocks to be removed by the sponge.
* Get a list of all blocks to be cleared by the sponge.
* <br>
* This list is mutable and contains the blocks in their removed state, i.e.
* having a type of {@link Material#AIR}.
* having a type of {@link Material#AIR} or not waterlogged.
*
* @return list of the to be removed blocks.
* @return list of the cleared blocks.
*/
@NotNull
public List<BlockState> getBlocks() {

View File

@@ -26353,7 +26353,7 @@ index 302841522cf990c38b1493b716048c0f2db40726..7932a6676db7b652d63be5ae4dcf9bcf
}
}
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
index 74c1c6f04a99817e67642cb330534c518830392f..8e9988130d9b912e5b858cd7792bdcefdeb66ada 100644
index 92a39927d7bd9af3ab67c2595d8ed435a2da6a69..7c9a2eed4441f816723562e0012f918db265912e 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -55,7 +55,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
@@ -28633,7 +28633,7 @@ index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b896
+ // Paper end - block counting
}
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index b1bebf925d4c1f8aa94917cffe878f092b89ad72..cd052ebeb963f1c6e717dfdc0397ec187d15b180 100644
index a97de8eddd764e628d0f3866c5d1e99e6dae5f6e..503ac3a16c8490d7a620d43b12c4f0c80565615a 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -147,7 +147,7 @@ import net.minecraft.world.waypoints.WaypointTransmitter;
@@ -29761,7 +29761,7 @@ index 300f3ed58109219d97846082941b860585f66fed..9175a7e4e6076626cb46144c5858c2f2
// Paper start - Affects Spawning API
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index fa56dd1bdca53f7667dc6b4b2c8634483bae5262..b799d9cc54c45920a7c48d5302454c9cec3ab7c2 100644
index 1eb8cb187d33510a4e99d229e721a2e7db4012ad..f286dd9996590e5d448ca809c34b6f640203e274 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -81,6 +81,7 @@ import net.minecraft.world.level.storage.LevelData;
@@ -30497,7 +30497,7 @@ index fa56dd1bdca53f7667dc6b4b2c8634483bae5262..b799d9cc54c45920a7c48d5302454c9c
}
// Paper end - Option to prevent armor stands from doing entity lookups
@@ -994,7 +1642,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
@@ -987,7 +1635,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
if (this.isOutsideBuildHeight(pos)) {
return null;
} else {
@@ -30506,7 +30506,7 @@ index fa56dd1bdca53f7667dc6b4b2c8634483bae5262..b799d9cc54c45920a7c48d5302454c9c
? null
: this.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE);
}
@@ -1087,22 +1735,16 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
@@ -1080,22 +1728,16 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
public List<Entity> getEntities(@Nullable Entity entity, AABB boundingBox, Predicate<? super Entity> predicate) {
Profiler.get().incrementCounter("getEntities");
List<Entity> list = Lists.newArrayList();
@@ -30537,7 +30537,7 @@ index fa56dd1bdca53f7667dc6b4b2c8634483bae5262..b799d9cc54c45920a7c48d5302454c9c
}
@Override
@@ -1116,33 +1758,94 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
@@ -1109,33 +1751,94 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
this.getEntities(entityTypeTest, bounds, predicate, output, Integer.MAX_VALUE);
}

View File

@@ -2352,10 +2352,10 @@ index f9c96bbdc54e68b9216b7f8662bfae03012d2866..34b7769663e235b93c6388ab0c92c00f
@Override
public void onCreated(Entity entity) {
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index b799d9cc54c45920a7c48d5302454c9cec3ab7c2..e4b9a564aad3d9b673808caa18265b06592ceab8 100644
index f286dd9996590e5d448ca809c34b6f640203e274..c41df4b1fff1f65532256e835dc30fadbb4f8c8b 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -2100,6 +2100,17 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
@@ -2093,6 +2093,17 @@ public abstract class Level implements LevelAccessor, UUIDLookup<Entity>, AutoCl
return 0;
}

View File

@@ -547,23 +547,16 @@
public boolean shouldTickDeath(Entity entity) {
return true;
@@ -608,6 +_,19 @@
@@ -608,6 +_,12 @@
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
+ // CraftBukkit start
+ return this.getBlockEntity(pos, true);
+ }
+
+ @Nullable
+ public BlockEntity getBlockEntity(BlockPos pos, boolean validate) {
+ // Paper start - Perf: Optimize capturedTileEntities lookup
+ net.minecraft.world.level.block.entity.BlockEntity blockEntity;
+ if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) {
+ return blockEntity;
+ }
+ // Paper end - Perf: Optimize capturedTileEntities lookup
+ // CraftBukkit end
if (this.isOutsideBuildHeight(pos)) {
return null;
} else {

View File

@@ -33,7 +33,7 @@
} else {
if (!blockState.is(Blocks.KELP)
&& !blockState.is(Blocks.KELP_PLANT)
@@ -81,16 +_,55 @@
@@ -81,16 +_,49 @@
return BlockPos.TraversalNodeStatus.SKIP;
}
@@ -43,7 +43,6 @@
+ // CraftBukkit start
+ // BlockEntity blockEntity = blockState.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
+ // dropResources(blockState, level, blockPos, blockEntity);
+ // level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
+ blockList.setBlock(blockPos, Blocks.AIR.defaultBlockState(), 3);
+ // CraftBukkit end
}
@@ -67,22 +66,17 @@
+
+ for (org.bukkit.craftbukkit.block.CraftBlockState snapshot : snapshots) {
+ BlockPos blockPos = snapshot.getPosition();
+ BlockState state = level.getBlockState(blockPos);
+ FluidState fluid = level.getFluidState(blockPos);
+ BlockState blockState = level.getBlockState(blockPos);
+ FluidState fluidState = level.getFluidState(blockPos);
+
+ if (fluid.is(FluidTags.WATER)) {
+ if (state.getBlock() instanceof BucketPickup bucketPickup && !bucketPickup.pickupBlock(null, level, blockPos, state).isEmpty()) {
+ // NOP
+ } else if (state.getBlock() instanceof LiquidBlock) {
+ // NOP
+ } else if (state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT) || state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS)) {
+ BlockEntity blockEntity = state.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
+ if (!fluidState.is(FluidTags.WATER)) {
+ } else if (blockState.getBlock() instanceof BucketPickup bucketPickup && !bucketPickup.pickupBlock(null, org.bukkit.craftbukkit.util.DummyGeneratorAccess.INSTANCE, blockPos, blockState).isEmpty()) {
+ } else if (blockState.getBlock() instanceof LiquidBlock) {
+ } else if (blockState.is(Blocks.KELP) || blockState.is(Blocks.KELP_PLANT) || blockState.is(Blocks.SEAGRASS) || blockState.is(Blocks.TALL_SEAGRASS)) {
+ BlockEntity blockEntity = blockState.hasBlockEntity() ? level.getBlockEntity(blockPos) : null;
+
+ // Paper start - Fix SpongeAbsortEvent handling
+ if (snapshot.getHandle().isAir()) {
+ dropResources(state, level, blockPos, blockEntity);
+ }
+ // Paper end - Fix SpongeAbsortEvent handling
+ if (snapshot.getHandle().isAir()) {
+ dropResources(blockState, level, blockPos, blockEntity);
+ }
+ }
+ snapshot.place(snapshot.getFlags());

View File

@@ -140,7 +140,7 @@
BlockPos.betweenClosed(this.bottomLeft, this.bottomLeft.relative(Direction.UP, this.height - 1).relative(this.rightDir, this.width - 1))
+ .forEach(pos -> this.blocks.setBlock(pos, blockState, 18));
+ org.bukkit.event.world.PortalCreateEvent event = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) this.blocks.getSnapshotBlocks(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.FIRE);
+ level.getMinecraftWorld().getServer().server.getPluginManager().callEvent(event);
+ level.getMinecraftWorld().getServer().server.getPluginManager().callEvent(event); // todo the list is not really mutable here unlike other call and the portal frame is included
+
+ if (event.isCancelled()) {
+ return false;

View File

@@ -198,7 +198,6 @@ public abstract class CraftRegionAccessor implements RegionAccessor {
BlockPos pos = CraftLocation.toBlockPosition(location);
BlockStateListPopulator populator = new BlockStateListPopulator(this.getHandle());
boolean result = this.generateTree(populator, this.getHandle().getMinecraftWorld().getChunkSource().getGenerator(), pos, new RandomSourceWrapper(random), treeType);
populator.refreshTiles();
populator.placeSomeBlocks(predicate == null ? ($ -> true) : predicate);
return result;
}

View File

@@ -74,10 +74,6 @@ public abstract class CraftBlockEntityState<T extends BlockEntity> extends Craft
this.loadData(state.getSnapshotNBT());
}
public void refreshSnapshot() {
this.load(this.blockEntity);
}
private RegistryAccess getRegistryAccess() {
LevelAccessor worldHandle = this.getWorldHandle();
return (worldHandle != null) ? worldHandle.registryAccess() : CraftRegistry.getMinecraftRegistry();

View File

@@ -1,10 +1,9 @@
package org.bukkit.craftbukkit.entity;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.entity.Flying;
import org.bukkit.entity.Ghast;
public class CraftGhast extends CraftMob implements Ghast, CraftEnemy, Flying {
public class CraftGhast extends CraftMob implements Ghast, CraftEnemy {
public CraftGhast(CraftServer server, net.minecraft.world.entity.monster.Ghast entity) {
super(server, entity);

View File

@@ -4,11 +4,10 @@ import net.minecraft.Optionull;
import org.bukkit.Location;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.util.CraftLocation;
import org.bukkit.entity.Flying;
import org.bukkit.entity.Phantom;
import java.util.UUID;
public class CraftPhantom extends CraftMob implements Phantom, CraftEnemy, Flying {
public class CraftPhantom extends CraftMob implements Phantom, CraftEnemy {
public CraftPhantom(CraftServer server, net.minecraft.world.entity.monster.Phantom entity) {
super(server, entity);

View File

@@ -19,34 +19,30 @@ import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import org.bukkit.block.BlockState;
import org.bukkit.craftbukkit.block.CraftBlock;
import org.bukkit.craftbukkit.block.CraftBlockEntityState;
import org.bukkit.craftbukkit.block.CraftBlockState;
public class BlockStateListPopulator extends DummyGeneratorAccess {
private final LevelAccessor world;
private final Map<BlockPos, net.minecraft.world.level.block.state.BlockState> dataMap = new HashMap<>();
private final Map<BlockPos, CapturedBlock> dataMap = new LinkedHashMap<>();
private final Map<BlockPos, BlockEntity> entityMap = new HashMap<>();
private final LinkedHashMap<BlockPos, CraftBlockState> blocks;
private List<CraftBlockState> blocks;
public BlockStateListPopulator(LevelAccessor world) {
this(world, new LinkedHashMap<>());
}
private BlockStateListPopulator(LevelAccessor world, LinkedHashMap<BlockPos, CraftBlockState> blocks) {
this.world = world;
this.blocks = blocks;
}
@Override
public net.minecraft.world.level.block.state.BlockState getBlockState(BlockPos pos) {
net.minecraft.world.level.block.state.BlockState state = this.dataMap.get(pos);
return (state != null) ? state : this.world.getBlockState(pos);
CapturedBlock block = this.dataMap.get(pos);
return (block != null) ? block.state() : this.world.getBlockState(pos);
}
@Override
public FluidState getFluidState(BlockPos pos) {
net.minecraft.world.level.block.state.BlockState state = this.dataMap.get(pos);
return (state != null) ? state.getFluidState() : this.world.getFluidState(pos);
CapturedBlock block = this.dataMap.get(pos);
return (block != null) ? block.state().getFluidState() : this.world.getFluidState(pos);
}
@Override
@@ -63,21 +59,23 @@ public class BlockStateListPopulator extends DummyGeneratorAccess {
public boolean setBlock(BlockPos pos, net.minecraft.world.level.block.state.BlockState state, int flags, int recursionLeft) {
pos = pos.immutable();
// remove first to keep insertion order
this.blocks.remove(pos);
this.dataMap.remove(pos);
this.dataMap.put(pos, state);
if (state.hasBlockEntity()) {
this.entityMap.put(pos, ((EntityBlock) state.getBlock()).newBlockEntity(pos, state));
this.dataMap.put(pos, new CapturedBlock(state, flags));
if (state.getBlock() instanceof EntityBlock entityBlock) {
// based on LevelChunk#setBlockState
BlockEntity currentBlockEntity = this.getBlockEntity(pos);
final BlockEntity newBlockEntity;
if (currentBlockEntity != null && currentBlockEntity.isValidBlockState(state)) {
newBlockEntity = currentBlockEntity; // previous block entity is still valid for this block state
currentBlockEntity.setBlockState(state);
} else {
newBlockEntity = entityBlock.newBlockEntity(pos, state); // create a new one when the block change
}
this.entityMap.put(pos, newBlockEntity);
} else {
this.entityMap.put(pos, null);
}
// use 'this' to ensure that the block state is the correct TileState
CraftBlockState snapshot = (CraftBlockState) CraftBlock.at(this, pos).getState();
snapshot.setFlags(flags);
// set world handle to ensure that updated calls are done to the world and not to this populator
snapshot.setWorldHandle(this.world);
this.blocks.put(pos, snapshot);
return true;
}
@@ -86,11 +84,19 @@ public class BlockStateListPopulator extends DummyGeneratorAccess {
return this.world.getMinecraftWorld();
}
public void refreshTiles() {
for (CraftBlockState snapshot : this.blocks.values()) {
if (snapshot instanceof CraftBlockEntityState) {
((CraftBlockEntityState<?>) snapshot).refreshSnapshot();
}
@Override
public ServerLevel getLevel() {
return this.getMinecraftWorld();
}
private void iterateSnapshots(Consumer<CraftBlockState> callback) {
for (Map.Entry<BlockPos, CapturedBlock> entry : this.dataMap.entrySet()) {
// use 'this' to ensure that the block state is the correct TileState
CraftBlockState snapshot = (CraftBlockState) CraftBlock.at(this, entry.getKey()).getState();
snapshot.setFlags(entry.getValue().flags());
// set world handle to ensure that updated calls are done to the world and not to this populator
snapshot.setWorldHandle(this.world);
callback.accept(snapshot);
}
}
@@ -107,16 +113,21 @@ public class BlockStateListPopulator extends DummyGeneratorAccess {
}
public void placeSomeBlocks(Consumer<? super CraftBlockState> beforeRun, Predicate<? super BlockState> filter) {
for (CraftBlockState state : this.blocks.values()) {
if (filter.test(state)) {
beforeRun.accept(state);
state.place(state.getFlags());
for (CraftBlockState snapshot : this.getSnapshotBlocks()) {
if (filter.test(snapshot)) {
beforeRun.accept(snapshot);
snapshot.place(snapshot.getFlags());
}
}
}
public List<CraftBlockState> getSnapshotBlocks() {
return new ArrayList<>(this.blocks.values());
if (this.blocks == null) {
List<CraftBlockState> blocks = new ArrayList<>();
this.iterateSnapshots(blocks::add);
this.blocks = blocks;
}
return blocks;
}
// For tree generation

View File

@@ -0,0 +1,6 @@
package org.bukkit.craftbukkit.util;
import net.minecraft.world.level.block.state.BlockState;
public record CapturedBlock(BlockState state, int flags) {
}