Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

# IntelliJ
out/

# Eclipse / VS Code Java language server build output
bin/
# mpeltonen/sbt-idea plugin
.idea_modules/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ public static void moveBlocks(final ServerLevel level, final AssemblyTransform t
container.setLootTable(null);
}
Clearable.tryClear(blockEntity);
if (blockEntity != null && !(blockEntity instanceof Clearable)) {
// The destination copy already owns the contents; leaving them in the source block entity
// would make the destruction below drop (or void) a duplicate via onRemove(...).
level.removeBlockEntity(block);
}
}

final LevelChunk chunk = resultingAccelerator.getChunk(SectionPos.blockToSectionCoord(newPos.getX()), SectionPos.blockToSectionCoord(newPos.getZ()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public void processSubLevelRemovals() {
for (final SubLevel subLevel : this.allSubLevels) {
if (subLevel instanceof final ServerSubLevel serverSubLevel) {
if (!serverSubLevel.isRemoved() && serverSubLevel.getMassTracker().isInvalid()) {
serverSubLevel.getPlot().destroyAllBlocks();
serverSubLevel.getPlot().destroyAllBlocks(false);
serverSubLevel.markRemoved();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,12 @@ public void kickAllEntities() {

/**
* Destroys all blocks within the plot
*
* @param dropItems whether the destroyed blocks should drop their items. Mass invalidation must pass
* {@code false}: at that point the plot only ever contains air and zero-mass blocks, which the
* disassembly sweep has already moved back to the world.
*/
public void destroyAllBlocks() {
public void destroyAllBlocks(boolean dropItems) {
if (this.localBounds == null || this.localBounds == BoundingBox3i.EMPTY) {
return;
}
Expand All @@ -305,7 +309,20 @@ public void destroyAllBlocks() {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
final BlockPos pos = new BlockPos(x, y, z);

level.destroyBlock(pos, true);
if (dropItems) {
level.destroyBlock(pos, true);
} else {
final BlockState state = level.getBlockState(pos);
if (state.isAir()) {
continue;
}
// Detach the block entity first so onRemove(...) cannot spill container contents. Neighbor
// updates are suppressed as well: nothing needs to react to a wholesale plot deletion, and
// updates can pop foreign blocks living in adjacent plot grid cells (dropping them).
level.removeBlockEntity(pos);
level.setBlock(pos, state.getFluidState().createLegacyBlock(),
Block.UPDATE_CLIENTS | Block.UPDATE_MOVE_BY_PISTON);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ private void split() {

// Protect against split sub-levels that have zero mass.
if (subLevel.getSelfMassTracker().getCenterOfMass() == null || subLevel.getSelfMassTracker().getMass() <= 0.0) {
subLevel.getPlot().destroyAllBlocks();
subLevel.getPlot().destroyAllBlocks(true);

final SubLevelContainer container = Objects.requireNonNull(SubLevelContainer.getContainer(level));
container.removeSubLevel(subLevel, SubLevelRemovalReason.REMOVED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ public void updateMassDataFromBlockChange(final SubLevel subLevel, final BlockPo
if (oldMass != 0.0) massTracker.addBlockMass(level, oldState, globalBlockPos, -oldMass, oldInertia);

if (!subLevel.isRemoved() && massTracker.isInvalid()) {
serverSubLevel.getPlot().destroyAllBlocks();
serverSubLevel.getPlot().destroyAllBlocks(false);
serverSubLevel.markRemoved();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.WallTorchBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.Property;
Expand Down Expand Up @@ -118,6 +121,81 @@ public static void testBrittleBreaking(final GameTestHelper helper) {
});
}

/**
* Regression test: demolishing an invalidated (massless) plot must not drop anything — the weightless blocks
* left in the plot are duplicates of blocks the disassembly sweep has already moved back to the world.
*/
@GameTest(template = "brittlebreak")
public static void testMasslessInvalidationDoesNotDrop(final GameTestHelper helper) {
final ServerLevel level = helper.getLevel();
final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level);
if (plotContainer == null) {
throw new IllegalStateException("Plot container not found in level");
}

final BlockPos min = helper.absolutePos(new BlockPos(0, 1, 0));
final BlockPos max = helper.absolutePos(new BlockPos(2, 3, 2));
final BoundingBox3i bounds = new BoundingBox3i(
min.getX(), min.getY(), min.getZ(),
max.getX(), max.getY(), max.getZ()
);

// A single stone with a zero-mass wall torch on each of its four sides (covering every facing).
final BlockPos stonePos = new BlockPos(1, 1, 1);

// Reset the template area — the floor layer included, or stray template decorations can drop during the
// run — so the test does not depend on template contents. Shape updates are suppressed (flag 16): clearing
// with normal flags would pop the template's adjacent redstone components and drop their items.
for (final BlockPos pos : BlockPos.betweenClosed(new BlockPos(0, 0, 0), new BlockPos(2, 3, 2))) {
level.setBlock(helper.absolutePos(pos), Blocks.AIR.defaultBlockState(),
Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
}
helper.setBlock(stonePos, Blocks.STONE.defaultBlockState());
for (final Direction direction : Direction.Plane.HORIZONTAL) {
helper.setBlock(stonePos.relative(direction),
Blocks.WALL_TORCH.defaultBlockState().setValue(WallTorchBlock.FACING, direction));
}

final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, min, BlockPos.betweenClosed(min, max), bounds);

helper.runAtTickTime(10, () -> {
final Level plot = subLevel.getLevel();
final BoundingBox3ic plotBounds = subLevel.getPlot().getBoundingBox();

// Remove the only mass-bearing block. Shape updates are suppressed (flag 16), matching silent
// disassembly sweeps, so the zero-mass torches are not popped first. This invalidates the mass
// tracker, and the physics system demolishes the plot through the real invalidation path.
for (final BlockPos pos : BlockPos.betweenClosed(plotBounds.minX(), plotBounds.minY(), plotBounds.minZ(),
plotBounds.maxX(), plotBounds.maxY(), plotBounds.maxZ())) {
if (plot.getBlockState(pos).isAir() || plot.getBlockState(pos).getBlock() == Blocks.WALL_TORCH) {
continue;
}
plot.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
}

// Fallback demolition in case the mass update hook did not fire; a no-op if the plot is already gone.
// Demolishing spawns the drops at the plot's position in the level, far away from the test structure,
// so the assertion has to cover the whole level. The template reset above keeps it free of stray items.
helper.runAtTickTime(20, () -> {
if (!subLevel.isRemoved()) {
subLevel.getPlot().destroyAllBlocks(false);
}

helper.runAtTickTime(30, () -> {
// Only the torches this test placed count as evidence: the shared plot grid can pop foreign
// decorations from neighbouring grid content during assembly, which drops unrelated items.
final List<? extends ItemEntity> droppedTorches = level.getEntities(EntityType.ITEM,
itemEntity -> itemEntity.getItem().is(Blocks.WALL_TORCH.asItem()));
if (!droppedTorches.isEmpty()) {
helper.fail("Demolishing the invalidated plot dropped " + droppedTorches.size()
+ " zero-mass wall torch item(s)");
}
helper.succeed();
});
});
});
}

@GameTest(template = "allblocks", required = false, manualOnly = true, timeoutTicks = 30_000_000)
public static void testAllBlocks(final GameTestHelper helper) {
final boolean failOnFirstError = false;
Expand Down