Compare commits

...
Author SHA1 Message Date
Owen1212055 6409d5ce9e Update 2026-06-26 20:54:44 +02:00
Owen1212055 2de25adfc9 Merge remote-tracking branch 'origin/main' into feat/block-capturing
# Conflicts:
#	paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
#	paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java
#	paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
#	paper-server/src/main/java/org/bukkit/craftbukkit/util/BlockStateListPopulator.java
2026-04-25 17:46:29 -04:00
Lulu13022002 4731020c9d only grow water plant on solid blocks, prevent item removal when event is cancelled 2026-02-15 20:04:50 +01:00
Lulu13022002 dbd8747173 allow to bonemeal bamboo and moss block 2026-02-14 20:02:59 +01:00
Lulu13022002 d4895653fa bunch of bonemeal fixes
Track bonemeal usage for StructureGrowEvent and propagate context properly between the two events.
Call the StructureGrowEvent for fungus, only call the BlockFertilizeEvent if the placement is sucessful.
2026-02-13 20:31:06 +01:00
Lulu13022002 4397b319a7 fix relative pos for placed block on water 2026-02-12 22:28:19 +01:00
Lulu13022002 298a8bfd32 remove CapturedBlockState and capturedFlags field 2026-02-12 21:06:58 +01:00
Lulu13022002 bcf5a3fffe allow to bonemeal the ocean floor 2026-02-11 22:20:10 +01:00
Lulu13022002 84a73d8984 format 2026-02-11 21:17:17 +01:00
Owen1212055 f46fbbe20b Fix block against logic
TODO: Lillypad logic looks odd
2026-02-06 17:40:37 -05:00
Owen1212055 ad45c84240 Bye treetype field 2026-02-06 14:48:11 -05:00
Owen1212055 e4114b0e0e Adjust pre cancel 2026-02-06 14:38:26 -05:00
Owen1212055 d56b548552 Fix some niche issues 2026-02-06 14:21:55 -05:00
Owen1212055 8e32dcbfe7 Fix some layering issues, simplify a bit 2026-02-06 12:32:57 -05:00
Owen1212055 6968eb065e Small name cleanup 2026-02-06 10:31:01 -05:00
Owen1212055 d55864cc09 Fix block entity placement 2026-02-06 10:08:51 -05:00
Owen1212055 c77b8418bf Better bonemeal support 2026-02-06 10:04:38 -05:00
Owen1212055 f4074a073e Fix build 2026-02-05 17:05:08 -05:00
Owen1212055 05eb2e4543 Block Capturing System 2026-02-05 16:52:05 -05:00
26 changed files with 3417 additions and 198 deletions

No files matched your search

@@ -10,8 +10,7 @@ import org.jetbrains.annotations.NotNull;
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.
*/
@@ -23838,7 +23838,7 @@ index 3c1490ac7c259da04031db2f170e0c0a5f512191..470d7c770ae9d045b97e2df145cfe3cf
}
}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896c81fc2ef 100644
index c11e2ce21e43f677bfccb65e6b5ad374912381fa..9aae782f8f9f1c0f7eae394ec0a1bda79e93f6f4 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -191,7 +191,7 @@ import net.minecraft.world.scores.ScoreboardSaveData;
@@ -23943,7 +23943,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
public MinecraftServer(
// CraftBukkit start
@@ -843,7 +929,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -853,7 +939,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
level.setSpawnSettings(level.isSpawningMonsters()); // Paper - per level difficulty (from setDifficulty(ServerLevel, Difficulty, boolean))
this.updateEffectiveRespawnData();
this.forceTicks = false; // CraftBukkit
@@ -23952,7 +23952,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
new org.bukkit.event.world.WorldLoadEvent(level.getWorld()).callEvent(); // Paper - call WorldLoadEvent
}
@@ -866,6 +952,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -876,6 +962,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public abstract boolean shouldRconBroadcast();
public boolean saveAllChunks(final boolean silent, final boolean flush, final boolean force) {
@@ -23964,7 +23964,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
this.scoreboard.storeToSaveDataIfDirty(this.getDataStorage().computeIfAbsent(ScoreboardSaveData.TYPE));
boolean result = false;
@@ -874,7 +965,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -884,7 +975,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
LOGGER.info("Saving chunks for level '{}'/{}", level, level.dimension().identifier());
}
@@ -23973,7 +23973,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
result = true;
}
@@ -971,7 +1062,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -981,7 +1072,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
@@ -23982,7 +23982,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
this.nextTickTimeNanos = Util.getNanos() + TimeUtil.NANOSECONDS_PER_MILLISECOND;
for (ServerLevel levelx : this.getAllLevels()) {
@@ -982,17 +1073,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -992,17 +1083,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.waitUntilNextTick();
}
@@ -24007,7 +24007,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
this.isSaving = false;
this.savedDataStorage.close();
@@ -1013,6 +1101,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1023,6 +1111,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.services().nameToIdCache().save(false); // Paper - Perf: Async GameProfileCache saving
}
// Spigot end
@@ -24022,7 +24022,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
// Paper start - Improved watchdog support - move final shutdown items here
Util.shutdownExecutors();
this.onServerExit();
@@ -1107,16 +1203,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1117,16 +1213,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// execute small amounts of other tasks just in case the number of tasks we are
// draining is large - chunk system and packet processing may be latency sensitive
@@ -24057,7 +24057,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
profiler.pop(); // moonrise:run_all_chunk
profiler.pop(); // moonrise:run_all_tasks
@@ -1417,6 +1528,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1427,6 +1538,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private boolean pollTaskInternal() {
if (super.pollTask()) {
@@ -24065,7 +24065,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
return true;
} else {
boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
@@ -1558,6 +1670,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1568,6 +1680,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper - improve tick loop - moved into runAllTasksAtTickStart
this.runAllTasksAtTickStart(); // Paper - improve tick loop
this.tickServer(sprinting ? () -> false : this::haveTime);
@@ -24079,7 +24079,7 @@ index e5925d778c28bd19ee4cb911fb115edc6351c26b..b4d7ccc2fd9e6c928445b09dbc6ac896
this.tickFrame.end();
this.recordEndOfTick(); // Paper - improve tick loop
profiler.pop();
@@ -2600,6 +2719,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -2610,6 +2729,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
@@ -30053,7 +30053,7 @@ index e07a7bda45146686a6ac2d20507df9fc8630514f..f84e651e2a34e76a0a71993332e4be1b
// Paper start - Affects Spawning API
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 81c3f9f04040b6cfa014ce55e517bd9b374ec252..0958fe0438c3e934b885acf78a0f8003ef8bb694 100644
index 30183d8752aa397a5c1fc554da49f595be286534..5566f449fb79b04822e533df4fd6de42116ffd14 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -87,6 +87,7 @@ import net.minecraft.world.level.storage.LevelData;
@@ -5,10 +5,10 @@ Subject: [PATCH] Incremental chunk and player saving
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 40a500011e7fcb6f5b07ff289f737ed9d3550296..da8c04aa02032c4c582709727f8f7f0b66b697b1 100644
index 1e88021d476710cf4fce9bf50c02993e076e36ee..8c4b40362f77cdea6e57315a6639006ac7ab27c7 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -958,7 +958,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -968,7 +968,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public boolean saveAllChunks(final boolean silent, final boolean flush, final boolean force, final boolean close) {
// Paper end - add close param
@@ -17,7 +17,7 @@ index 40a500011e7fcb6f5b07ff289f737ed9d3550296..da8c04aa02032c4c582709727f8f7f0b
boolean result = false;
for (ServerLevel level : this.getAllLevels()) {
@@ -970,13 +970,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -980,13 +980,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
result = true;
}
@@ -32,7 +32,7 @@ index 40a500011e7fcb6f5b07ff289f737ed9d3550296..da8c04aa02032c4c582709727f8f7f0b
if (flush) {
for (ServerLevel level : this.getAllLevels()) {
@@ -990,11 +984,25 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1000,11 +994,25 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return result;
}
@@ -59,7 +59,7 @@ index 40a500011e7fcb6f5b07ff289f737ed9d3550296..da8c04aa02032c4c582709727f8f7f0b
boolean result = this.saveAllChunks(silent, flush, force);
this.warnOnLowDiskSpace();
var5 = result;
@@ -1641,9 +1649,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1651,9 +1659,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
this.ticksUntilAutosave--;
@@ -20,10 +20,10 @@ index 2bc436cdf5180a7943c45fabb9fbbedae6f7db56..f312a7f5b1b2a777ab36b94ce7cbf387
@Override
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index da8c04aa02032c4c582709727f8f7f0b66b697b1..3923f21c67165e2ecd2c9cecd5266758435a6806 100644
index 8c4b40362f77cdea6e57315a6639006ac7ab27c7..a5217deb236ee922d020a7a6a03141a5f67acd77 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1784,32 +1784,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1794,32 +1794,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
@@ -48,10 +48,10 @@ index 0000000000000000000000000000000000000000..24a2090e068ad3c0d08705050944abdf
+ }
+}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 3923f21c67165e2ecd2c9cecd5266758435a6806..92acd256fdf815dfb8dd743cf8ecd0eb377d996e 100644
index a5217deb236ee922d020a7a6a03141a5f67acd77..4fdf7354bc01789cf7480b14c2fc8fe75b748563 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1836,6 +1836,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1846,6 +1846,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
level.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
level.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
level.updateLagCompensationTick(); // Paper - lag compensation
File diff suppressed because it is too large. Load diff
@@ -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;
}
}
}
@@ -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 blockState, @Block.UpdateFlags int updateFlags) {
this.add(new CaptureRecord(pos, blockState, updateFlags));
}
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.blockState;
}
// 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.blockState, 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 blockState;
private @Nullable BlockEntity blockEntity;
private boolean clearPreviousBlockEntity;
private @Block.UpdateFlags int updateFlags;
public CaptureRecord(BlockPos pos, BlockState blockState, BlockEntity blockEntity) {
this.pos = pos;
this.blockState = blockState;
this.blockEntity = blockEntity;
}
public CaptureRecord(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags) {
this.pos = pos;
this.blockState = blockState;
this.updateFlags = updateFlags;
}
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.blockState, this.updateFlags);
if (this.blockEntity != null) {
level.setBlockEntity(this.blockEntity);
}
}
public void setBlockEntity(boolean clearPreviousBlockEntity, @Nullable BlockEntity add) {
this.clearPreviousBlockEntity = clearPreviousBlockEntity;
this.blockEntity = add;
}
}
}
@@ -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);
}
}
}
@@ -0,0 +1,46 @@
package io.papermc.paper.util.capture;
import java.util.Optional;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
public record LayeredBlockPlacementPredictor(
BlockPlacementPredictor... predictors
) implements BlockPlacementPredictor {
@Override
public Optional<BlockState> getLatestBlockAt(BlockPos pos) {
for (BlockPlacementPredictor predictor : this.predictors) {
Optional<BlockState> state = predictor.getLatestBlockAt(pos);
if (state.isPresent()) {
return state;
}
}
return Optional.empty();
}
@Override
public Optional<LoadedBlockState> getLatestBlockAtIfLoaded(BlockPos pos) {
for (BlockPlacementPredictor predictor : this.predictors) {
Optional<LoadedBlockState> state = predictor.getLatestBlockAtIfLoaded(pos);
if (state.isPresent()) {
return state;
}
}
return Optional.empty();
}
@Override
public Optional<BlockEntityPlacement> getLatestBlockEntityAt(BlockPos pos) {
for (BlockPlacementPredictor predictor : this.predictors) {
Optional<BlockEntityPlacement> state = predictor.getLatestBlockEntityAt(pos);
if (state.isPresent()) {
return state;
}
}
return Optional.empty();
}
}
@@ -0,0 +1,46 @@
package io.papermc.paper.util.capture;
import java.util.Optional;
import java.util.function.Supplier;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.Nullable;
public record LiveBlockPlacementLayer(WorldCapturer capturer, ServerLevel level) implements BlockPlacementPredictor {
@Override
public Optional<BlockState> getLatestBlockAt(BlockPos pos) {
return Optional.of(provideLive(() -> this.level.getBlockState(pos)));
}
@Override
public Optional<LoadedBlockState> getLatestBlockAtIfLoaded(BlockPos pos) {
BlockState state = provideLive(() -> this.level.getBlockStateIfLoaded(pos));
if (state == null) {
return LoadedBlockState.UNLOADED;
}
return Optional.of(new LoadedBlockState(true, state));
}
@Override
public Optional<BlockEntityPlacement> getLatestBlockEntityAt(BlockPos pos) {
BlockEntity blockEntity = provideLive(() -> this.level.getBlockEntity(pos));
if (blockEntity == null) {
return BlockEntityPlacement.ABSENT;
}
return Optional.of(new BlockEntityPlacement(false, blockEntity));
}
public <T> @Nullable T provideLive(Supplier<@Nullable T> valueProvider) {
SimpleBlockCapture blockCapture = this.capturer.getCapture();
this.capturer.releaseCapture(null);
T value = valueProvider.get();
this.capturer.releaseCapture(blockCapture);
return value;
}
}
@@ -0,0 +1,448 @@
package io.papermc.paper.util.capture;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.jspecify.annotations.Nullable;
public class MinecraftCaptureBridge implements PaperCapturingWorldLevel {
private final ServerLevel parent;
private final List<Runnable> queuedTasks = new ArrayList<>();
// Effective represents plugin set blocks -> predicted blocks -> actual server level
private final BlockPlacementPredictor effectiveReadLayer;
private SimpleBlockPlacementPredictor writeLayer;
// This is the layer that is written to in the server level potentially.
// Mostly plugin set blocks
private final SimpleBlockPlacementPredictor serverLevelOverlayLayer;
private final CapturingTickAccess<Block> blocks;
private final CapturingTickAccess<Fluid> liquids;
private Consumer<Runnable> sink = this.queuedTasks::add;
public MinecraftCaptureBridge(ServerLevel parent, BlockPlacementPredictor baseReadLayer) {
this.parent = parent;
this.serverLevelOverlayLayer = new SimpleBlockPlacementPredictor();
SimpleBlockPlacementPredictor predictedBlocks = new SimpleBlockPlacementPredictor();
this.effectiveReadLayer = new LayeredBlockPlacementPredictor(
this.serverLevelOverlayLayer, // The overlay layer represents plugin set blocks!
predictedBlocks, // Now predicted blocks
baseReadLayer
);
this.writeLayer = predictedBlocks; // Predicting
this.blocks = new CapturingTickAccess<>(parent.getBlockTicks());
this.liquids = new CapturingTickAccess<>(parent.getFluidTicks());
}
@Override
public long getSeed() {
return this.parent.getSeed();
}
@Override
public ServerLevel getLevel() {
return this.parent;
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos pos) {
return this.parent.getCurrentDifficultyAt(pos);
}
@Override
public long nextSubTickCount() {
return this.parent.nextSubTickCount();
}
@Override
public LevelData getLevelData() {
return this.parent.getLevelData();
}
@Override
public @Nullable MinecraftServer getServer() {
return this.parent.getServer();
}
@Override
public ServerChunkCache getChunkSource() {
return this.parent.getChunkSource();
}
@Override
public boolean setBlockAndUpdate(BlockPos pos, BlockState blockState) {
return this.setBlock(pos, blockState, Block.UPDATE_ALL);
}
@Override
public SimpleBlockCapture forkCaptureSession() {
return this.parent.capturer.createCaptureSession(new BlockPlacementPredictor() {
@Override
public Optional<BlockState> getLatestBlockAt(BlockPos pos) {
return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockAt(pos);
}
@Override
public Optional<LoadedBlockState> getLatestBlockAtIfLoaded(BlockPos pos) {
return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockAtIfLoaded(pos);
}
@Override
public Optional<BlockEntityPlacement> getLatestBlockEntityAt(BlockPos pos) {
return MinecraftCaptureBridge.this.effectiveReadLayer.getLatestBlockEntityAt(pos);
}
});
}
@Override
public RandomSource getRandom() {
// TODO: Support rolling this back?
return this.parent.getRandom();
}
@Override
public void playSound(@Nullable Entity except, BlockPos pos, SoundEvent sound, SoundSource source, float volume, float pitch) {
this.addTask((level) -> level.playSound(except, pos, sound, source, volume, pitch));
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z, double xd, double yd, double zd) {
this.addTask((level) -> level.addParticle(particle, x, y, z, xd, yd, zd));
}
@Override
public void levelEvent(@Nullable Entity source, int type, BlockPos pos, int data) {
this.addTask((level) -> level.levelEvent(source, type, pos, data));
}
@Override
public void gameEvent(Holder<GameEvent> gameEvent, Vec3 position, GameEvent.Context context) {
this.addTask((level) -> level.gameEvent(gameEvent, position, context));
}
@Override
public LevelLightEngine getLightEngine() {
return this.parent.getLightEngine();
}
@Override
public WorldBorder getWorldBorder() {
return this.parent.getWorldBorder();
}
@Override
public @Nullable BlockEntity getBlockEntity(BlockPos pos) {
return this.effectiveReadLayer.getLatestBlockEntityAt(pos)
.map(BlockPlacementPredictor.BlockEntityPlacement::blockEntity)
.orElse(null);
}
@Override
public BlockState getBlockState(BlockPos pos) {
return this.effectiveReadLayer.getLatestBlockAt(pos).orElseThrow(); // Should not ever be null, parent should pass value
}
@Override
public @Nullable BlockState getBlockStateIfLoaded(BlockPos pos) {
return this.effectiveReadLayer.getLatestBlockAtIfLoaded(pos).map(BlockPlacementPredictor.LoadedBlockState::state).orElse(null);
}
@Override
public @Nullable FluidState getFluidIfLoaded(BlockPos pos) {
return this.getBlockStateIfLoaded(pos).getFluidState();
}
@Override
public FluidState getFluidState(BlockPos pos) {
return this.getBlockState(pos).getFluidState();
}
@Override
public List<Entity> getEntities(@Nullable Entity except, AABB bb, Predicate<? super Entity> selector) {
return this.parent.getEntities(except, bb, selector);
}
@Override
public <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> entityTypeTest, AABB bb, Predicate<? super T> selector) {
return this.parent.getEntities(entityTypeTest, bb, selector);
}
@Override
public List<? extends Player> players() {
return this.parent.players();
}
@Override
public @Nullable ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean loadOrGenerate) {
return this.parent.getChunk(chunkX, chunkZ, status, loadOrGenerate);
}
@Override
public @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
return this.parent.getChunkIfLoadedImmediately(x, z);
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
return this.parent.getHeight(type, x, z); // TODO?
}
@Override
public int getSkyDarken() {
return this.parent.getSkyDarken();
}
@Override
public BiomeManager getBiomeManager() {
return this.parent.getBiomeManager();
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
return this.parent.getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public boolean isClientSide() {
return this.parent.isClientSide();
}
@Override
public int getSeaLevel() {
return this.parent.getSeaLevel();
}
@Override
public DimensionType dimensionType() {
return this.parent.dimensionType();
}
@Override
public RegistryAccess registryAccess() {
return this.parent.registryAccess();
}
@Override
public FeatureFlagSet enabledFeatures() {
return this.parent.enabledFeatures();
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
return this.parent.environmentAttributes();
}
@Override
public boolean isStateAtPosition(BlockPos pos, Predicate<BlockState> predicate) {
return predicate.test(this.getBlockState(pos));
}
@Override
public boolean isFluidAtPosition(BlockPos pos, Predicate<FluidState> predicate) {
return predicate.test(this.getFluidState(pos));
}
public boolean silentSet(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags) {
return this.writeLayer.setBlockState(this.effectiveReadLayer, pos, blockState, updateFlags);
}
@Override
public boolean setBlock(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit) {
BlockPos copy = pos.immutable();
this.addTask((level) -> level.setBlock(copy, blockState, updateFlags, updateLimit));
return this.writeLayer.setBlockState(this.effectiveReadLayer, copy, blockState, updateFlags);
}
@Override
public boolean removeBlock(BlockPos pos, boolean movedByPiston) {
BlockPos copy = pos.immutable();
this.addTask((level) -> level.removeBlock(copy, movedByPiston));
FluidState fluidState = this.getFluidState(copy);
return this.silentSet(copy, fluidState.createLegacyBlock(), Block.UPDATE_ALL | (movedByPiston ? Block.UPDATE_MOVE_BY_PISTON : 0));
}
@Override
public boolean destroyBlock(BlockPos pos, boolean dropResources, @Nullable Entity breaker, int updateLimit) {
BlockPos copy = pos.immutable();
this.addTask((level) -> level.destroyBlock(copy, dropResources, breaker, updateLimit));
BlockState blockState = this.getBlockState(copy);
if (blockState.isAir()) {
return false;
} else {
FluidState fluidState = this.getFluidState(copy);
return this.silentSet(copy, fluidState.createLegacyBlock(), Block.UPDATE_ALL);
}
}
@Override
public void sendBlockUpdated(BlockPos pos, BlockState old, BlockState current, @Block.UpdateFlags int updateFlags) {
BlockPos copy = pos.immutable();
this.addTask((level) -> level.sendBlockUpdated(copy, old, current, updateFlags));
}
@Override
public void setBlockEntity(BlockEntity blockEntity) {
this.writeLayer.getRecordMap().setLatestBlockEntityAt(blockEntity.getBlockPos().immutable(), false, blockEntity);
}
@Override
public boolean setBlockSilent(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit) {
BlockPos copy = pos.immutable();
return this.silentSet(copy, blockState, updateFlags);
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return this.blocks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return this.liquids;
}
@Override
public GameRules getGameRules() {
return this.parent.getGameRules();
}
@Override
public void addTask(Consumer<ServerLevel> level) {
this.sink.accept(() -> level.accept(this.parent));
}
public @Nullable BlockState getLatestBlockState(BlockPos pos) {
return this.effectiveReadLayer.getLatestBlockAt(pos).orElse(null);
}
public @Nullable Optional<BlockEntity> getLatestBlockEntity(BlockPos pos) {
Optional<BlockPlacementPredictor.BlockEntityPlacement> placement = this.effectiveReadLayer.getLatestBlockEntityAt(pos);
if (placement.isEmpty() || (placement.get().blockEntity() == null && !placement.get().removed())) {
return null;
}
return placement.map(BlockPlacementPredictor.BlockEntityPlacement::blockEntity);
}
public List<org.bukkit.block.BlockState> calculateLatestSnapshots(ServerLevel level) {
return this.writeLayer.getRecordMap().calculateLatestSnapshots(level);
}
public Stream<org.bukkit.block.Block> getAffectedBlocks(ServerLevel level) {
return this.writeLayer.getRecordMap().getAffectedBlocks(level);
}
public void applyTasks() {
this.sink = Runnable::run;
for (Runnable runnable : this.queuedTasks) {
runnable.run();
}
this.blocks.apply();
this.liquids.apply();
// If we have changes that the plugin applied on top of the already existing changes, we know that we can apply them.
// So, do that!
if (!this.serverLevelOverlayLayer.isEmpty()) {
this.serverLevelOverlayLayer.getRecordMap().applyApiPatch(this.parent);
}
// Apply block entities, those may have been written in our estimation. So just apply them.
// I don't really like this, as I in general don't really want to apply things from the prediction layer.
// But, these may be mutated by anything.
if (!this.writeLayer.getRecordMap().isEmpty()) {
this.writeLayer.getRecordMap().applyBlockEntities(this.parent);
}
}
public void allowWriteOnLevel() {
this.writeLayer = this.serverLevelOverlayLayer;
}
public static class CapturingTickAccess<T> implements LevelTickAccess<T> {
private final LevelTickAccess<T> wrapped;
private final Set<BlockPos> scheduled = new HashSet<>();
private final List<ScheduledTick<T>> ticks = new ArrayList<>();
public CapturingTickAccess(LevelTickAccess<T> wrapped) {
this.wrapped = wrapped;
}
@Override
public boolean willTickThisTick(BlockPos pos, T type) {
return this.wrapped.willTickThisTick(pos, type);
}
@Override
public void schedule(ScheduledTick<T> tick) {
this.scheduled.add(tick.pos());
this.ticks.add(tick);
}
@Override
public boolean hasScheduledTick(BlockPos pos, T type) {
return this.wrapped.hasScheduledTick(pos, type) || this.scheduled.contains(pos);
}
@Override
public int count() {
return this.wrapped.count() + this.scheduled.size();
}
public void apply() {
this.ticks.forEach(this.wrapped::schedule);
}
}
}
@@ -0,0 +1,30 @@
package io.papermc.paper.util.capture;
import java.util.function.Consumer;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gamerules.GameRules;
public interface PaperCapturingWorldLevel extends WorldGenLevel {
GameRules getGameRules();
void sendBlockUpdated(BlockPos pos, BlockState old, BlockState current, @Block.UpdateFlags int updateFlags);
void setBlockEntity(BlockEntity blockEntity);
boolean setBlockSilent(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit);
ServerChunkCache getChunkSource();
boolean setBlockAndUpdate(BlockPos pos, BlockState blockState);
void addTask(Consumer<ServerLevel> level);
SimpleBlockCapture forkCaptureSession();
}
@@ -0,0 +1,47 @@
package io.papermc.paper.util.capture;
import java.util.function.Consumer;
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 net.minecraft.world.level.gamerules.GameRules;
public interface ServerLevelPaperCapturingWorldLevel extends PaperCapturingWorldLevel {
@Override
default GameRules getGameRules() {
return this.getLevel().getGameRules();
}
@Override
default void sendBlockUpdated(BlockPos pos, BlockState old, BlockState current, @Block.UpdateFlags int updateFlags) {
this.getLevel().sendBlockUpdated(pos, old, current, updateFlags);
}
@Override
default void setBlockEntity(BlockEntity blockEntity) {
this.getLevel().setBlockEntity(blockEntity);
}
@Override
default boolean setBlockSilent(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit) {
return this.getLevel().setBlock(pos, blockState, updateFlags, updateLimit);
}
@Override
default boolean setBlockAndUpdate(BlockPos pos, BlockState blockState) {
return this.getLevel().setBlockAndUpdate(pos, blockState);
}
@Override
default void addTask(Consumer<ServerLevel> level) {
level.accept(this.getLevel());
}
@Override
default SimpleBlockCapture forkCaptureSession() {
return this.getLevel().capturer.createCaptureSession();
}
}
@@ -0,0 +1,74 @@
package io.papermc.paper.util.capture;
import java.util.List;
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.entity.BlockEntity;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.jspecify.annotations.Nullable;
public class SimpleBlockCapture implements AutoCloseable {
private final MinecraftCaptureBridge capturingWorldLevel;
private final ServerLevel level;
private final @Nullable SimpleBlockCapture oldCapture;
private boolean isOverlayingCaptureOnLevel = false;
public SimpleBlockCapture(BlockPlacementPredictor base, ServerLevel level, @Nullable SimpleBlockCapture oldCapture) {
this.capturingWorldLevel = new MinecraftCaptureBridge(level, base);
this.level = level;
this.oldCapture = oldCapture;
}
public MinecraftCaptureBridge capturingWorldLevel() {
return this.capturingWorldLevel;
}
public boolean isCapturing() {
return true;
}
public List<BlockState> getCapturedSnapshots() {
return this.capturingWorldLevel.calculateLatestSnapshots(this.level);
}
public Stream<Block> getAffectedBlocks() {
return this.capturingWorldLevel.getAffectedBlocks(this.level);
}
public net.minecraft.world.level.block.state.@Nullable BlockState getOverlayBlockState(BlockPos pos) {
return this.capturingWorldLevel.getLatestBlockState(pos);
}
public @Nullable Optional<BlockEntity> getOverlayBlockEntity(BlockPos pos) {
return this.capturingWorldLevel.getLatestBlockEntity(pos);
}
// This is done so that the captured blocks appear ontop of the world.
public void overlayCaptureOnLevel() {
this.isOverlayingCaptureOnLevel = true;
this.capturingWorldLevel.allowWriteOnLevel();
}
public boolean isOverlayingCaptureOnLevel() {
return this.isOverlayingCaptureOnLevel;
}
public void finalizePlacement() {
this.level.capturer.releaseCapture(this.oldCapture);
this.capturingWorldLevel.applyTasks();
}
@Override
public void close() {
this.level.capturer.releaseCapture(this.oldCapture);
}
public net.minecraft.world.level.block.state.@Nullable BlockState getCaptureBlockStateIfLoaded(BlockPos pos) {
return this.capturingWorldLevel.getBlockStateIfLoaded(pos);
}
}
@@ -0,0 +1,109 @@
package io.papermc.paper.util.capture;
import java.util.Optional;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Block;
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;
// Block state
class SimpleBlockPlacementPredictor implements BlockPlacementPredictor {
private final CaptureRecordMap guesstimationMap = new CaptureRecordMap();
public boolean setBlockState(BlockPlacementPredictor layer, BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags) {
BlockState oldState = layer.getLatestBlockAt(pos).orElse(Blocks.AIR.defaultBlockState());
// Don't do any processing if the same
if (oldState == blockState) {
return false;
} else {
Block newBlock = blockState.getBlock();
this.setLatestBlockAt(pos, blockState, updateFlags);
// TODO: local heightmaps?
// this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING).update(i, y, i2, blockState);
// this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES).update(i, y, i2, blockState);
// this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR).update(i, y, i2, blockState);
// this.heightmaps.get(Heightmap.Types.WORLD_SURFACE).update(i, y, i2, blockState);
// LIGHT ENGINE CALCULATIONS
boolean blockChanged = !oldState.is(newBlock);
if (blockChanged && oldState.hasBlockEntity() && !blockState.shouldChangedStateKeepBlockEntity(oldState)) {
this.removeBlockEntity(pos);
}
// if ((blockChanged || newBlock instanceof BaseRailBlock) && ((updateFlags & Block.UPDATE_NEIGHBORS) != 0 || updateMoveByPiston)) {
// BlockState finalOldState = oldState;
// this.capturingWorldLevel.addTask((level) -> {
// finalOldState.affectNeighborsAfterRemoval(level, pos, updateMoveByPiston);
// });
// }
if (blockState.hasBlockEntity()) {
BlockEntity blockEntity = this.getLatestBlockEntityAt(pos).map(BlockEntityPlacement::blockEntity).orElse(null);
if (blockEntity != null && !blockEntity.isValidBlockState(blockState)) {
blockEntity = null;
}
if (blockEntity == null) {
blockEntity = ((EntityBlock) newBlock).newBlockEntity(pos, blockState);
if (blockEntity != null) {
this.addAndRegisterBlockEntity(blockEntity);
}
} else {
blockEntity.setBlockState(blockState);
}
}
}
return true;
}
private void addAndRegisterBlockEntity(BlockEntity blockEntity) {
this.guesstimationMap.setLatestBlockEntityAt(blockEntity.getBlockPos(), false, blockEntity);
}
private void removeBlockEntity(BlockPos pos) {
this.guesstimationMap.setLatestBlockEntityAt(pos, true, null);
}
public boolean isEmpty() {
return this.guesstimationMap.isEmpty();
}
@Override
public Optional<BlockEntityPlacement> getLatestBlockEntityAt(BlockPos pos) {
Optional<BlockEntity> value = this.guesstimationMap.getLatestBlockEntityAt(pos);
if (value == null) {
return Optional.empty();
} else {
return value
.map(block -> new BlockEntityPlacement(false, block))
.or(() -> BlockEntityPlacement.ABSENT);
}
}
@Override
public Optional<BlockState> getLatestBlockAt(BlockPos pos) {
return Optional.ofNullable(this.guesstimationMap.getLatestBlockStateAt(pos));
}
@Override
public Optional<LoadedBlockState> getLatestBlockAtIfLoaded(BlockPos pos) {
return Optional.ofNullable(this.guesstimationMap.getLatestBlockStateAt(pos))
.map((state) -> new LoadedBlockState(true, state));
}
public void setLatestBlockAt(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags) {
this.guesstimationMap.setLatestBlockStateAt(pos, blockState, updateFlags);
}
public CaptureRecordMap getRecordMap() {
return this.guesstimationMap;
}
}
@@ -0,0 +1,38 @@
package io.papermc.paper.util.capture;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.Nullable;
// TODO: Cleanup this state, because its held on the server world. I had proper state handling but threw it away at some point
public class WorldCapturer {
public final ServerLevel level;
private @Nullable SimpleBlockCapture capture;
public WorldCapturer(Level level) {
this.level = (ServerLevel) level;
}
public SimpleBlockCapture createCaptureSession(BlockPlacementPredictor blockPlacementPredictor) {
this.capture = new SimpleBlockCapture(blockPlacementPredictor, this.level, this.capture);
return this.capture;
}
public SimpleBlockCapture createCaptureSession() {
return this.createCaptureSession(new LiveBlockPlacementLayer(this, this.level));
}
public void releaseCapture(@Nullable SimpleBlockCapture oldCapture) {
this.capture = oldCapture;
}
public @Nullable SimpleBlockCapture getCapture() {
return this.capture;
}
public boolean isCapturing() {
return this.capture != null;
}
}
@@ -0,0 +1,4 @@
@NullMarked
package io.papermc.paper.util.capture;
import org.jspecify.annotations.NullMarked;
@@ -17,6 +17,8 @@ import io.papermc.paper.raytracing.PositionedRayTraceConfigurationBuilderImpl;
import io.papermc.paper.raytracing.RayTraceTarget;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.util.capture.MinecraftCaptureBridge;
import io.papermc.paper.util.capture.SimpleBlockCapture;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -60,14 +62,12 @@ import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.NullOps;
import net.minecraft.util.TriState;
import net.minecraft.world.attribute.BedRule;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LightningBolt;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.raid.Raids;
@@ -110,7 +110,6 @@ import org.bukkit.block.data.BlockData;
import org.bukkit.boss.DragonBattle;
import org.bukkit.craftbukkit.block.CraftBiome;
import org.bukkit.craftbukkit.block.CraftBlock;
import org.bukkit.craftbukkit.block.CraftBlockState;
import org.bukkit.craftbukkit.block.CraftBlockType;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.boss.CraftDragonBattle;
@@ -130,6 +129,7 @@ import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.craftbukkit.util.CraftRayTraceResult;
import org.bukkit.craftbukkit.util.CraftSpawnCategory;
import org.bukkit.craftbukkit.util.CraftStructureSearchResult;
import org.bukkit.craftbukkit.util.RandomSourceWrapper;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
@@ -746,26 +746,20 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) {
this.world.captureTreeGeneration = true;
this.world.captureBlockStates = true;
boolean grownTree = this.generateTree(loc, type);
this.world.captureBlockStates = false;
this.world.captureTreeGeneration = false;
if (grownTree) { // Copy block data to delegate
for (BlockState blockstate : this.world.capturedBlockStates.values()) {
BlockPos position = ((CraftBlockState) blockstate).getPosition();
net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position);
int flags = ((CraftBlockState) blockstate).getFlags();
delegate.setBlockData(blockstate.getX(), blockstate.getY(), blockstate.getZ(), blockstate.getBlockData());
net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position);
this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flags, net.minecraft.world.level.block.Block.UPDATE_LIMIT);
}
this.world.capturedBlockStates.clear();
return true;
} else {
this.world.capturedBlockStates.clear();
return false;
}
try (SimpleBlockCapture capture = this.world.forkCaptureSession()) {
MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel();
BlockPos pos = CraftLocation.toBlockPos(loc);
boolean generated = this.generateTree(captureTreeGeneration, this.getHandle().getMinecraftWorld().getChunkSource().getGenerator(), pos, new RandomSourceWrapper(CraftWorld.rand), type);
if (generated) {
List<BlockState> blocks = captureTreeGeneration.calculateLatestSnapshots(this.world);
for (BlockState state : blocks) {
delegate.setBlockData(state.getX(), state.getY(), state.getZ(), state.getBlockData());
}
}
return generated;
}
}
@Override
@@ -1,80 +0,0 @@
package org.bukkit.craftbukkit.block;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@Deprecated(forRemoval = true)
public final class CapturedBlockState extends CraftBlockState {
private final boolean treeBlock;
public CapturedBlockState(Block block, @net.minecraft.world.level.block.Block.UpdateFlags int capturedFlags, boolean treeBlock) {
super(block, capturedFlags);
this.treeBlock = treeBlock;
}
private CapturedBlockState(CapturedBlockState state, Location location) {
super(state, location);
this.treeBlock = state.treeBlock;
}
@Override
public boolean update(boolean force, boolean applyPhysics) {
boolean result = super.update(force, applyPhysics);
if (result) {
this.addBees();
}
return result;
}
@Override
public boolean place(@net.minecraft.world.level.block.Block.UpdateFlags int flags) {
boolean result = super.place(flags);
this.addBees();
return result;
}
private void addBees() {
// SPIGOT-5537: Horrible hack to manually add bees given Level#captureTreeGeneration does not support block entities
if (this.treeBlock && this.getType() == Material.BEE_NEST) {
WorldGenLevel worldGenLevel = this.world.getHandle();
BlockPos pos = this.getPosition();
RandomSource randomSource = worldGenLevel.getRandom();
// Begin copied block from BeehiveDecorator
worldGenLevel.getBlockEntity(pos, BlockEntityType.BEEHIVE).ifPresent(beehiveBlockEntity -> {
int i1 = 2 + randomSource.nextInt(2);
for (int i2 = 0; i2 < i1; i2++) {
beehiveBlockEntity.storeBee(BeehiveBlockEntity.Occupant.create(randomSource.nextInt(599)));
}
});
// End copied block
}
}
@Override
public CapturedBlockState copy() {
return new CapturedBlockState(this, null);
}
@Override
public CapturedBlockState copy(Location location) {
return new CapturedBlockState(this, location);
}
public static CapturedBlockState getTreeBlockState(Level world, BlockPos pos, @net.minecraft.world.level.block.Block.UpdateFlags int flags) {
return new CapturedBlockState(CraftBlock.at(world, pos), flags, true);
}
}
@@ -521,41 +521,10 @@ public class CraftBlock implements Block {
Direction direction = blockFaceToNotch(face);
Preconditions.checkArgument(direction != null, face + " is not a valid cartesian face");
BlockFertilizeEvent event = null;
ServerLevel world = this.getCraftWorld().getHandle();
UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false));
// SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent
world.captureTreeGeneration = true;
InteractionResult result = BoneMealItem.applyBonemeal(context);
world.captureTreeGeneration = false;
if (!world.capturedBlockStates.isEmpty()) {
TreeType treeType = SaplingBlock.treeType;
SaplingBlock.treeType = null;
List<org.bukkit.block.BlockState> states = new ArrayList<>(world.capturedBlockStates.values());
world.capturedBlockStates.clear();
StructureGrowEvent structureEvent = null;
if (treeType != null) {
structureEvent = new StructureGrowEvent(this.getLocation(), treeType, true, null, states);
Bukkit.getPluginManager().callEvent(structureEvent);
}
event = new BlockFertilizeEvent(CraftBlock.at(world, this.getPosition()), null, states);
event.setCancelled(structureEvent != null && structureEvent.isCancelled());
Bukkit.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
for (org.bukkit.block.BlockState state : states) {
CraftBlockState craftBlockState = (CraftBlockState) state;
craftBlockState.place(craftBlockState.getFlags());
world.checkCapturedTreeStateForObserverNotify(this.position, craftBlockState);
}
}
}
return result == InteractionResult.SUCCESS && (event == null || !event.isCancelled());
return BoneMealItem.applyBonemeal(context) == InteractionResult.SUCCESS;
}
@Override
@@ -28,21 +28,16 @@ public class CraftBlockState implements BlockState {
protected final CraftWorld world;
private final BlockPos position;
protected net.minecraft.world.level.block.state.BlockState block;
@net.minecraft.world.level.block.Block.UpdateFlags
protected int capturedFlags; // todo move out of this class
private WeakReference<LevelAccessor> weakLevel;
protected CraftBlockState(final Block block) {
this(block.getWorld(), ((CraftBlock) block).getPosition(), ((CraftBlock) block).getBlockState());
this.capturedFlags = net.minecraft.world.level.block.Block.UPDATE_ALL;
this.setWorldHandle(((CraftBlock) block).getLevel());
}
@Deprecated
protected CraftBlockState(final Block block, @net.minecraft.world.level.block.Block.UpdateFlags int capturedFlags) {
this(block);
this.capturedFlags = capturedFlags;
}
// world can be null for non-placed BlockStates.
@@ -62,7 +57,6 @@ public class CraftBlockState implements BlockState {
this.position = CraftLocation.toBlockPos(location);
}
this.block = from.block;
this.capturedFlags = from.capturedFlags;
this.setWorldHandle(from.getWorldHandle());
}
@@ -181,14 +175,6 @@ public class CraftBlockState implements BlockState {
return this.block.getBukkitMaterial();
}
public void setFlags(@net.minecraft.world.level.block.Block.UpdateFlags int flags) {
this.capturedFlags = flags;
}
public @net.minecraft.world.level.block.Block.UpdateFlags int getFlags() {
return this.capturedFlags;
}
@Override
public byte getLightLevel() {
return this.getBlock().getLightLevel();
@@ -252,17 +238,6 @@ public class CraftBlockState implements BlockState {
return this.getWorldHandle().setBlock(this.position, this.block, flags);
}
// used to revert a block placement due to an event being cancelled for example
public boolean revertPlace() {
return this.place(
net.minecraft.world.level.block.Block.UPDATE_CLIENTS |
net.minecraft.world.level.block.Block.UPDATE_KNOWN_SHAPE |
net.minecraft.world.level.block.Block.UPDATE_SUPPRESS_DROPS |
net.minecraft.world.level.block.Block.UPDATE_SKIP_ON_PLACE |
net.minecraft.world.level.block.Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS
);
}
@Override
public byte getRawData() {
return CraftMagicNumbers.toLegacyData(this.block);
@@ -15,6 +15,10 @@ import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent;
import io.papermc.paper.event.player.PlayerBedFailEnterEvent;
import io.papermc.paper.event.player.PlayerToggleEntityAgeLockEvent;
import io.papermc.paper.util.capture.GrowthContext;
import io.papermc.paper.util.capture.MinecraftCaptureBridge;
import io.papermc.paper.util.capture.PaperCapturingWorldLevel;
import io.papermc.paper.util.capture.SimpleBlockCapture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
@@ -108,6 +112,7 @@ import org.bukkit.craftbukkit.inventory.CraftInventoryCrafting;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.inventory.CraftItemType;
import org.bukkit.craftbukkit.potion.CraftPotionUtil;
import org.bukkit.craftbukkit.util.CraftLocation;
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.craftbukkit.util.CraftVector;
import org.bukkit.entity.AbstractHorse;
@@ -148,6 +153,7 @@ import org.bukkit.event.block.BlockDispenseLootEvent;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockFadeEvent;
import org.bukkit.event.block.BlockFertilizeEvent;
import org.bukkit.event.block.BlockFormEvent;
import org.bukkit.event.block.BlockGrowEvent;
import org.bukkit.event.block.BlockIgniteEvent;
@@ -264,6 +270,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
import org.bukkit.event.world.EntitiesLoadEvent;
import org.bukkit.event.world.EntitiesUnloadEvent;
import org.bukkit.event.world.LootGenerateEvent;
import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.inventory.CraftingRecipe;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.InventoryView;
@@ -490,20 +497,20 @@ public class CraftEventFactory {
return event.getResonatedEntities().stream().map((bukkitEntity) -> ((CraftLivingEntity) bukkitEntity).getHandle());
}
public static BlockMultiPlaceEvent callBlockMultiPlaceEvent(ServerLevel level, net.minecraft.world.entity.player.Player player, InteractionHand hand, List<BlockState> blockStates, BlockPos clickedPos) {
public static BlockMultiPlaceEvent callBlockMultiPlaceEvent(ServerLevel level, net.minecraft.world.entity.player.Player player, InteractionHand hand, List<BlockState> replacedSnapshots, BlockPos clickedPos) {
Player cplayer = (Player) player.getBukkitEntity();
Block clickedBlock = CraftBlock.at(level, clickedPos);
boolean canBuild = true;
for (BlockState blockState : blockStates) {
if (!CraftEventFactory.canBuild(level, cplayer, blockState.getX(), blockState.getZ())) {
for (BlockState snapshot : replacedSnapshots) {
if (!CraftEventFactory.canBuild(level, cplayer, snapshot.getX(), snapshot.getZ())) {
canBuild = false;
break;
}
}
EquipmentSlot handSlot = CraftEquipmentSlot.getHand(hand);
BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(blockStates, clickedBlock, cplayer.getInventory().getItem(handSlot), cplayer, canBuild, handSlot);
BlockMultiPlaceEvent event = new BlockMultiPlaceEvent(replacedSnapshots, clickedBlock, cplayer.getInventory().getItem(handSlot), cplayer, canBuild, handSlot);
event.callEvent();
return event;
@@ -1278,7 +1285,7 @@ public class CraftEventFactory {
return event;
}
public static boolean handleBlockGrowEvent(Level level, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @net.minecraft.world.level.block.Block.UpdateFlags int flags) {
public static boolean handleBlockGrowEvent(LevelAccessor level, BlockPos pos, net.minecraft.world.level.block.state.BlockState state, @net.minecraft.world.level.block.Block.UpdateFlags int flags) {
CraftBlockState snapshot = CraftBlockStates.getBlockState(level, pos);
snapshot.setBlock(state);
@@ -2397,6 +2404,54 @@ public class CraftEventFactory {
return false;
}
// todo block list is sometimes empty for trees in both events
public static boolean structureEvent(PaperCapturingWorldLevel level, BlockPos pos, Function<PaperCapturingWorldLevel, Boolean> worldGenCapture, GrowthContext context) {
ServerLevel originalLevel = level.getLevel();
try (SimpleBlockCapture capture = level.forkCaptureSession()) {
MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel();
if (worldGenCapture.apply(captureTreeGeneration)) {
Location location = CraftLocation.toBukkit(pos, originalLevel);
List<BlockState> blocks = captureTreeGeneration.calculateLatestSnapshots(originalLevel);
StructureGrowEvent event = new StructureGrowEvent(location, context.getTreeSpecies(), context.usedBoneMeal(), context.getBukkitPlayer(), blocks);
event.setCancelled(context.cancelled);
if (event.callEvent()) {
capture.finalizePlacement(); // todo block list is mutable
return true;
} else {
context.cancelled = true;
}
} else {
capture.finalizePlacement();
}
}
return false;
}
// todo cancelling fertilize event doesn't work for azalea
public static boolean fertilizeBlock(ServerLevel level, BlockPos pos, Function<PaperCapturingWorldLevel, Boolean> worldGenCapture, GrowthContext context) {
try (SimpleBlockCapture capture = level.forkCaptureSession()) {
MinecraftCaptureBridge captureTreeGeneration = capture.capturingWorldLevel();
if (worldGenCapture.apply(captureTreeGeneration)) {
List<BlockState> blocks = captureTreeGeneration.calculateLatestSnapshots(level);
BlockFertilizeEvent event = new BlockFertilizeEvent(CraftBlock.at(level, pos), context.getBukkitPlayer(), blocks);
event.setCancelled(context.cancelled);
if (event.callEvent()) {
capture.finalizePlacement(); // todo block list is mutable
return true;
} else {
context.cancelled = true;
}
} else {
capture.finalizePlacement();
}
}
return false;
}
public static boolean callPlayerToggleEntityAgeLockEvent(net.minecraft.world.entity.player.Player player, Mob target, ItemStack itemUsed, InteractionHand hand, boolean ageLocked, @Nullable EntityDataAccessor<?> accessorToResync) {
PlayerToggleEntityAgeLockEvent event = new PlayerToggleEntityAgeLockEvent(
(org.bukkit.entity.Player) player.getBukkitEntity(),
@@ -18,11 +18,11 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import org.bukkit.block.BlockState;
import org.bukkit.craftbukkit.block.CraftBlockState;
import org.bukkit.craftbukkit.block.CraftBlockStates;
@@ -38,7 +38,7 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
}
@Override
public net.minecraft.world.level.block.state.BlockState getBlockState(BlockPos pos) {
public BlockState getBlockState(BlockPos pos) {
CapturedBlock block = this.blocks.get(pos);
return block != null ? block.state() : this.level.getBlockState(pos);
}
@@ -56,7 +56,7 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
}
@Override
public boolean setBlock(BlockPos pos, net.minecraft.world.level.block.state.BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit) {
public boolean setBlock(BlockPos pos, BlockState blockState, @Block.UpdateFlags int updateFlags, int updateLimit) {
pos = pos.immutable();
// remove first to keep last updated order
this.blocks.remove(pos);
@@ -81,7 +81,7 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
@Override
public boolean destroyBlock(BlockPos pos, boolean dropResources, Entity breaker, int updateLimit) {
net.minecraft.world.level.block.state.BlockState blockState = this.getBlockState(pos);
BlockState blockState = this.getBlockState(pos);
if (blockState.isAir()) {
return false;
}
@@ -101,7 +101,6 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
CraftBlockState snapshot = CraftBlockStates.getBlockState(
this.getMinecraftWorld().getWorld(), entry.getKey(), block.state(), block.blockEntity()
);
snapshot.setFlags(block.flags());
snapshot.setWorldHandle(this.level);
callback.accept(snapshot);
}
@@ -111,7 +110,7 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
this.placeSomeBlocks(_ -> true);
}
public void placeSomeBlocks(Predicate<? super BlockState> filter) {
public void placeSomeBlocks(Predicate<? super CraftBlockState> filter) {
this.placeSomeBlocks(_ -> {}, filter);
}
@@ -119,22 +118,28 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
this.placeSomeBlocks(beforeRun, _ -> true);
}
public void placeSomeBlocks(Consumer<? super CraftBlockState> beforeRun, Predicate<? super BlockState> filter) {
public void placeSomeBlocks(Consumer<? super CraftBlockState> beforeRun, Predicate<? super CraftBlockState> filter) {
for (CraftBlockState snapshot : this.getSnapshotBlocks()) {
if (filter.test(snapshot)) {
int flags = this.getEffectiveFlags(snapshot.getPosition());
beforeRun.accept(snapshot);
snapshot.place(snapshot.getFlags());
snapshot.place(flags);
}
}
}
public @Block.UpdateFlags int getEffectiveFlags(BlockPos pos) {
CapturedBlock block = this.blocks.get(pos);
return block != null ? block.flags() : Block.UPDATE_ALL; // fallback for new API-added blocks
}
public List<CraftBlockState> getSnapshotBlocks() {
if (this.snapshots == null) {
List<CraftBlockState> snapshots = new ArrayList<>();
this.iterateSnapshots(snapshots::add);
this.snapshots = snapshots;
}
return snapshots;
return this.snapshots;
}
// For tree generation
@@ -155,7 +160,7 @@ public class BlockStateListPopulator extends DummyLevelAccessor {
}
@Override
public boolean isStateAtPosition(BlockPos pos, Predicate<net.minecraft.world.level.block.state.BlockState> predicate) {
public boolean isStateAtPosition(BlockPos pos, Predicate<BlockState> predicate) {
return predicate.test(this.getBlockState(pos));
}
@@ -0,0 +1,86 @@
package io.papermc.testplugin;
import org.bukkit.Material;
import org.bukkit.TreeType;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockFertilizeEvent;
import org.bukkit.event.block.BlockMultiPlaceEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.event.world.StructureGrowEvent;
import static net.kyori.adventure.text.Component.text;
public record OwenPlayground() implements Listener {
public static final OwenPlayground INSTANCE = new OwenPlayground();
@EventHandler
public void on(BlockFertilizeEvent event) {
//event.setCancelled(true);
// event.getPlayer().sendBlockChanges(event.getBlocks());
//
// new BukkitRunnable(){
//
// @Override
// public void run() {
// event.getBlocks().forEach((state) -> event.getPlayer().sendBlockChange(state.getLocation(), state.getBlock().getType().createBlockData()));
// }
// }.runTaskLater(this, 20 * 2);
}
@EventHandler
public void on(StructureGrowEvent event) {
// event.setCancelled(true);
//
// event.getPlayer().sendBlockChanges(event.getBlocks());
//
// new BukkitRunnable(){
//
// @Override
// public void run() {
// event.getBlocks().forEach((state) -> event.getPlayer().sendBlockChange(state.getLocation(), state.getBlock().getType().createBlockData()));
// }
// }.runTaskLater(this, 20 * 2);
}
@EventHandler
public void on(PlayerSwapHandItemsEvent event) {
event.getPlayer().getTargetBlockExact(5).applyBoneMeal(BlockFace.UP);
event.getPlayer().getWorld().generateTree(event.getPlayer().getLocation(), TreeType.values()[(int) (TreeType.values().length * Math.random())]);
}
@EventHandler
public void on(BlockPlaceEvent event) {
event.getPlayer().sendActionBar(text("Replaced: " + event.getBlockReplacedState().getType()));
if (event.getPlayer().getInventory().contains(Material.DIAMOND)) {
event.getBlock().setType(Material.AIR);
} else if (event.getPlayer().getInventory().contains(Material.GOLD_INGOT)) {
event.setCancelled(true);
} else if (event.getPlayer().getInventory().contains(Material.EMERALD)) {
event.getBlock().setType(Material.STONE);
} else if (event.getPlayer().getInventory().contains(Material.GOLD_NUGGET)) {
event.getBlock().getRelative(BlockFace.SOUTH).setType(Material.STONE);
}
event.getPlayer().sendMessage(event.getBlock().getType().toString());
event.getBlockAgainst().setType(Material.DIAMOND_BLOCK);
}
@EventHandler
public void on(BlockMultiPlaceEvent event) {
event.getPlayer().sendActionBar(text("Replaced: " + event.getReplacedBlockStates().stream().map(BlockState::getBlockData).toList()));
event.getPlayer().sendMessage(event.getBlock().getType().toString());
if (event.getPlayer().getInventory().contains(Material.DIAMOND)) {
event.setCancelled(true);
}
}
}
@@ -8,6 +8,7 @@ public final class TestPlugin extends JavaPlugin implements Listener {
@Override
public void onEnable() {
this.getServer().getPluginManager().registerEvents(this, this);
this.getServer().getPluginManager().registerEvents(OwenPlayground.INSTANCE, this);
// io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this);
}