Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import org.jetbrains.annotations.Nullable;

/**
* Called when a block is ignited. If you want to catch when a Player places
* fire, you need to use {@link BlockPlaceEvent}.
* Called when a block is ignited.
* <p>
* If this event is cancelled, the block will not be ignited.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
*/
public class BlockMultiPlaceEvent extends BlockPlaceEvent {

private final List<BlockState> states;
private final List<BlockState> replacedStates;

@ApiStatus.Internal
@Deprecated(forRemoval = true)
public BlockMultiPlaceEvent(@NotNull List<BlockState> states, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild) {
this(states, clicked, itemInHand, thePlayer, canBuild, org.bukkit.inventory.EquipmentSlot.HAND);
public BlockMultiPlaceEvent(@NotNull List<BlockState> replacedStates, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild) {
this(replacedStates, clicked, itemInHand, thePlayer, canBuild, org.bukkit.inventory.EquipmentSlot.HAND);
}

@ApiStatus.Internal
public BlockMultiPlaceEvent(@NotNull List<BlockState> states, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild, @NotNull org.bukkit.inventory.EquipmentSlot hand) {
super(states.get(0).getBlock(), states.get(0), clicked, itemInHand, thePlayer, canBuild, hand);
this.states = ImmutableList.copyOf(states);
public BlockMultiPlaceEvent(@NotNull List<BlockState> replacedStates, @NotNull Block clicked, @NotNull ItemStack itemInHand, @NotNull Player thePlayer, boolean canBuild, @NotNull org.bukkit.inventory.EquipmentSlot hand) {
super(replacedStates.getFirst().getBlock(), replacedStates.getFirst(), clicked, itemInHand, thePlayer, canBuild, hand);
this.replacedStates = ImmutableList.copyOf(replacedStates);
}

/**
Expand All @@ -41,6 +41,6 @@ public BlockMultiPlaceEvent(@NotNull List<BlockState> states, @NotNull Block cli
*/
@NotNull
public List<BlockState> getReplacedBlockStates() {
return this.states;
return this.replacedStates;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,25 @@ public class BlockPlaceEvent extends BlockEvent implements Cancellable {
protected Block placedAgainst;
protected ItemStack itemInHand;
protected Player player;
protected BlockState replacedBlockState;
protected BlockState replacedState;
protected boolean canBuild;
protected EquipmentSlot hand;

protected boolean cancelled;

@ApiStatus.Internal
@Deprecated(since = "1.9", forRemoval = true)
public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedBlockState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild) {
this(placedBlock, replacedBlockState, placedAgainst, itemInHand, thePlayer, canBuild, EquipmentSlot.HAND);
public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild) {
this(placedBlock, replacedState, placedAgainst, itemInHand, thePlayer, canBuild, EquipmentSlot.HAND);
}

@ApiStatus.Internal
public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedBlockState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild, @NotNull final EquipmentSlot hand) {
public BlockPlaceEvent(@NotNull final Block placedBlock, @NotNull final BlockState replacedState, @NotNull final Block placedAgainst, @NotNull final ItemStack itemInHand, @NotNull final Player thePlayer, final boolean canBuild, @NotNull final EquipmentSlot hand) {
super(placedBlock);
this.placedAgainst = placedAgainst;
this.itemInHand = itemInHand;
this.player = thePlayer;
this.replacedBlockState = replacedBlockState;
this.replacedState = replacedState;
this.canBuild = canBuild;
this.hand = hand;
}
Expand Down Expand Up @@ -95,7 +95,7 @@ public Block getBlockPlaced() {
*/
@NotNull
public BlockState getBlockReplacedState() {
return this.replacedBlockState;
return this.replacedState;
}

/**
Expand Down
2,147 changes: 2,147 additions & 0 deletions paper-server/patches/features/0032-Block-Capture-System.patch

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.papermc.paper.util.capture;

import java.util.Optional;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.Nullable;

public interface BlockPlacementPredictor {

Optional<BlockState> getLatestBlockAt(BlockPos pos);

Optional<LoadedBlockState> getLatestBlockAtIfLoaded(BlockPos pos);

Optional<BlockEntityPlacement> getLatestBlockEntityAt(BlockPos pos);

record BlockEntityPlacement(boolean removed, @Nullable BlockEntity blockEntity) {

public static final Optional<BlockEntityPlacement> ABSENT = Optional.of(new BlockEntityPlacement(false, null));

public @Nullable BlockEntity res() {
return this.removed ? null : this.blockEntity;
}
}

record LoadedBlockState(boolean present, @Nullable BlockState state) {

public static final Optional<LoadedBlockState> UNLOADED = Optional.of(new LoadedBlockState(false, null));

public @Nullable BlockState res() {
return this.present ? this.state : null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package io.papermc.paper.util.capture;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.craftbukkit.block.CraftBlock;
import org.bukkit.craftbukkit.block.CraftBlockStates;
import org.jspecify.annotations.Nullable;

/*
The premise of this storage is to essentially "mock" block placement in the game.
This should attempt to mirror block placement the best it can, in order to provide a read solution of possibly what blocks
are going to be placed in the world.

The reason why we use a custom capture map rather than just storing the blocks placed in the world is to ensure
that blocks are placed in the correct order, and that the vanilla block chain can occur, which includes physics updates.
*/
public final class CaptureRecordMap {

private final Map<BlockPos, CaptureRecord> recordsByPos = new HashMap<>();

public void setLatestBlockStateAt(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) {
this.add(new CaptureRecord(pos, state, flags));
}

public void setLatestBlockEntityAt(BlockPos pos, boolean clearPreviousBlockEntity, @Nullable BlockEntity add) {
CaptureRecord oldRecord = this.recordsByPos.get(pos);
if (oldRecord != null) {
oldRecord.setBlockEntity(clearPreviousBlockEntity, add);
}
}

private void add(CaptureRecord record) {
this.recordsByPos.put(record.pos, record);
}

public boolean isEmpty() {
return this.recordsByPos.isEmpty();
}

public @Nullable BlockState getLatestBlockStateAt(BlockPos pos) {
CaptureRecord record = this.recordsByPos.get(pos);
if (record == null) {
return null;
}

return record.state;
}

// Null indicates that it's not present, no override
// Optional empty indicates its being removed
public @Nullable Optional<BlockEntity> getLatestBlockEntityAt(BlockPos pos) {
CaptureRecord record = this.recordsByPos.get(pos);
if (record == null) {
return null;
}

return Optional.ofNullable(record.blockEntity);
}

public void applyBlockEntities(ServerLevel parent) {
this.recordsByPos.keySet().forEach((pos) -> {
Optional<BlockEntity> res = this.getLatestBlockEntityAt(pos);
if (res != null && res.isPresent()) {
parent.setBlockEntity(res.get());
}
});
}

public void applyApiPatch(ServerLevel level) {
this.recordsByPos.keySet().forEach((pos) -> {
this.recordsByPos.get(pos).applyApiPatch(level);
});
}

// TODO: Clean this up
public List<org.bukkit.block.BlockState> calculateLatestSnapshots(ServerLevel level) {
List<org.bukkit.block.BlockState> out = new ArrayList<>();

for (Map.Entry<BlockPos, CaptureRecord> entry : this.recordsByPos.entrySet()) {
CaptureRecord captureRecord = entry.getValue();
out.add(CraftBlockStates.getBlockState(level.getWorld(), entry.getKey(), captureRecord.state, captureRecord.blockEntity));
}
return out;
}

public Stream<org.bukkit.block.Block> getAffectedBlocks(ServerLevel level) {
return this.recordsByPos.keySet().stream().map(pos -> CraftBlock.at(level, pos));
}

public static class CaptureRecord {

private final BlockPos pos;

private BlockState state;
private @Nullable BlockEntity blockEntity;
private boolean clearPreviousBlockEntity;
private @Block.UpdateFlags int flags;

public CaptureRecord(BlockPos pos, BlockState state, BlockEntity blockEntity) {
this.pos = pos;
this.state = state;
this.blockEntity = blockEntity;
}

public CaptureRecord(BlockPos pos, BlockState state, @Block.UpdateFlags int flags) {
this.pos = pos;
this.state = state;
this.flags = flags;
}

public CaptureRecord(boolean remove, @Nullable BlockEntity add, BlockPos pos) {
this.clearPreviousBlockEntity = remove;
this.blockEntity = add;
this.pos = pos;
}

public void applyApiPatch(ServerLevel level) {
if (this.clearPreviousBlockEntity) {
level.removeBlockEntity(this.pos);
}

level.setBlock(this.pos, this.state, this.flags);
if (this.blockEntity != null) {
level.setBlockEntity(this.blockEntity);
}
}

public void setBlockEntity(boolean clearPreviousBlockEntity, @Nullable BlockEntity add) {
this.clearPreviousBlockEntity = clearPreviousBlockEntity;
this.blockEntity = add;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.papermc.paper.util.capture;

import net.minecraft.Optionull;
import net.minecraft.core.Holder;
import net.minecraft.data.worldgen.features.TreeFeatures;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import org.bukkit.TreeType;
import org.jspecify.annotations.Nullable;

public final class GrowthContext {

public static GrowthContext empty() {
return new GrowthContext(null, false);
}

public static GrowthContext usingBoneMeal(@Nullable Player user) {
return new GrowthContext(user, true);
}

private final @Nullable Player player;
private final boolean usedBoneMeal;

public boolean cancelled = false;
public Holder<ConfiguredFeature<?, ?>> feature; // just make this a field

private GrowthContext(@Nullable Player player, boolean usedBoneMeal) {
this.player = player;
this.usedBoneMeal = usedBoneMeal;
}

public org.bukkit.entity.@Nullable Player getBukkitPlayer() {
return (org.bukkit.entity.Player) Optionull.map(this.player, Entity::getBukkitEntity);
}

public boolean usedBoneMeal() {
return this.usedBoneMeal;
}

public TreeType getTreeSpecies() {
return getTreeSpecies(this.feature);
}

private static TreeType getTreeSpecies(Holder<ConfiguredFeature<?, ?>> feature) {
if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) {
return TreeType.RED_MUSHROOM;
} else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) {
return TreeType.BROWN_MUSHROOM;
} else if (feature.is(TreeFeatures.WARPED_FUNGUS_PLANTED)) {
return TreeType.WARPED_FUNGUS;
} else if (feature.is(TreeFeatures.CRIMSON_FUNGUS_PLANTED)) {
return TreeType.CRIMSON_FUNGUS;
} else if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) {
return TreeType.TREE;
} else if (feature.is(TreeFeatures.JUNGLE_TREE)) {
return TreeType.COCOA_TREE;
} else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) {
return TreeType.SMALL_JUNGLE;
} else if (feature.is(TreeFeatures.PINE)) {
return TreeType.TALL_REDWOOD;
} else if (feature.is(TreeFeatures.SPRUCE)) {
return TreeType.REDWOOD;
} else if (feature.is(TreeFeatures.ACACIA)) {
return TreeType.ACACIA;
} else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) {
return TreeType.BIRCH;
} else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) {
return TreeType.TALL_BIRCH;
} else if (feature.is(TreeFeatures.SWAMP_OAK)) {
return TreeType.SWAMP;
} else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) {
return TreeType.BIG_TREE;
} else if (feature.is(TreeFeatures.JUNGLE_BUSH)) {
return TreeType.JUNGLE_BUSH;
} else if (feature.is(TreeFeatures.DARK_OAK)) {
return TreeType.DARK_OAK;
} else if (feature.is(TreeFeatures.MEGA_SPRUCE)) {
return TreeType.MEGA_REDWOOD;
} else if (feature.is(TreeFeatures.MEGA_PINE)) {
return TreeType.MEGA_PINE;
} else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) {
return TreeType.JUNGLE;
} else if (feature.is(TreeFeatures.AZALEA_TREE)) {
return TreeType.AZALEA;
} else if (feature.is(TreeFeatures.MANGROVE)) {
return TreeType.MANGROVE;
} else if (feature.is(TreeFeatures.TALL_MANGROVE)) {
return TreeType.TALL_MANGROVE;
} else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) {
return TreeType.CHERRY;
} else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) {
return TreeType.PALE_OAK;
} else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) {
return TreeType.PALE_OAK_CREAKING;
} else {
throw new IllegalArgumentException("Unknown tree feature: " + feature);
}
}
}
Loading
Loading