package com.sk89q.worldedit.bukkit.adapter.impl.fawe.v1_19_R3; import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.math.IntPair; import com.fastasyncworldedit.core.util.TaskManager; import com.fastasyncworldedit.core.util.task.RunnableVal; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.internal.block.BlockStateIdAccess; import com.sk89q.worldedit.internal.wna.WorldNativeAccess; import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; import com.sk89q.worldedit.world.block.BlockState; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ServerChunkCache; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.chunk.LevelChunk; import org.bukkit.craftbukkit.v1_19_R3.CraftWorld; import org.bukkit.craftbukkit.v1_19_R3.block.data.CraftBlockData; import org.bukkit.event.block.BlockPhysicsEvent; import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; public class PaperweightFaweWorldNativeAccess implements WorldNativeAccess { private static final int UPDATE = 1; private static final int NOTIFY = 2; private static final Direction[] NEIGHBOUR_ORDER = { Direction.EAST, Direction.WEST, Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH }; private final PaperweightFaweAdapter paperweightFaweAdapter; private final WeakReference level; private final AtomicInteger lastTick; private final Set cachedChanges = new HashSet<>(); private final Set cachedChunksToSend = new HashSet<>(); private SideEffectSet sideEffectSet; public PaperweightFaweWorldNativeAccess(PaperweightFaweAdapter paperweightFaweAdapter, WeakReference level) { this.paperweightFaweAdapter = paperweightFaweAdapter; this.level = level; // Use the actual tick as minecraft-defined so we don't try to force blocks into the world when the server's already lagging. // - With the caveat that we don't want to have too many cached changed (1024) so we'd flush those at 1024 anyway. this.lastTick = new AtomicInteger(MinecraftServer.currentTick); } private Level getLevel() { return Objects.requireNonNull(level.get(), "The reference to the world was lost"); } @Override public void setCurrentSideEffectSet(SideEffectSet sideEffectSet) { this.sideEffectSet = sideEffectSet; } @Override public LevelChunk getChunk(int x, int z) { return getLevel().getChunk(x, z); } @Override public net.minecraft.world.level.block.state.BlockState toNative(BlockState blockState) { int stateId = paperweightFaweAdapter.ordinalToIbdID(blockState.getOrdinalChar()); return BlockStateIdAccess.isValidInternalId(stateId) ? Block.stateById(stateId) : ((CraftBlockData) BukkitAdapter.adapt(blockState)).getState(); } @Override public net.minecraft.world.level.block.state.BlockState getBlockState(LevelChunk levelChunk, BlockPos blockPos) { return levelChunk.getBlockState(blockPos); } @Nullable @Override public synchronized net.minecraft.world.level.block.state.BlockState setBlockState( LevelChunk levelChunk, BlockPos blockPos, net.minecraft.world.level.block.state.BlockState blockState ) { int currentTick = MinecraftServer.currentTick; if (Fawe.isMainThread()) { return levelChunk.setBlockState(blockPos, blockState, this.sideEffectSet != null && this.sideEffectSet.shouldApply(SideEffect.UPDATE) ); } // Since FAWE is.. Async we need to do it on the main thread (wooooo.. :( ) cachedChanges.add(new CachedChange(levelChunk, blockPos, blockState)); cachedChunksToSend.add(new IntPair(levelChunk.bukkitChunk.getX(), levelChunk.bukkitChunk.getZ())); boolean nextTick = lastTick.get() > currentTick; if (nextTick || cachedChanges.size() >= 1024) { if (nextTick) { lastTick.set(currentTick); } flushAsync(nextTick); } return blockState; } @Override public net.minecraft.world.level.block.state.BlockState getValidBlockForPosition( net.minecraft.world.level.block.state.BlockState blockState, BlockPos blockPos ) { return Block.updateFromNeighbourShapes(blockState, getLevel(), blockPos); } @Override public BlockPos getPosition(int x, int y, int z) { return new BlockPos(x, y, z); } @Override public void updateLightingForBlock(BlockPos blockPos) { getLevel().getChunkSource().getLightEngine().checkBlock(blockPos); } @Override public boolean updateTileEntity(BlockPos blockPos, CompoundBinaryTag tag) { // We will assume that the tile entity was created for us, // though we do not do this on the other versions BlockEntity blockEntity = getLevel().getBlockEntity(blockPos); if (blockEntity == null) { return false; } net.minecraft.nbt.Tag nativeTag = paperweightFaweAdapter.fromNativeBinary(tag); blockEntity.load((CompoundTag) nativeTag); return true; } @Override public void notifyBlockUpdate( LevelChunk levelChunk, BlockPos blockPos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState ) { if (levelChunk.getSections()[level.get().getSectionIndex(blockPos.getY())] != null) { getLevel().sendBlockUpdated(blockPos, oldState, newState, UPDATE | NOTIFY); } } @Override public boolean isChunkTicking(LevelChunk levelChunk) { return levelChunk.getFullStatus().isOrAfter(ChunkHolder.FullChunkStatus.TICKING); } @Override public void markBlockChanged(LevelChunk levelChunk, BlockPos blockPos) { if (levelChunk.getSections()[level.get().getSectionIndex(blockPos.getY())] != null) { ((ServerChunkCache) getLevel().getChunkSource()).blockChanged(blockPos); } } @Override public void notifyNeighbors( BlockPos blockPos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState ) { Level level = getLevel(); if (sideEffectSet.shouldApply(SideEffect.EVENTS)) { level.blockUpdated(blockPos, oldState.getBlock()); } else { // When we don't want events, manually run the physics without them. // Un-nest neighbour updating for (Direction direction : NEIGHBOUR_ORDER) { BlockPos shifted = blockPos.relative(direction); level.getBlockState(shifted).neighborChanged(level, shifted, oldState.getBlock(), blockPos, false); } } if (newState.hasAnalogOutputSignal()) { level.updateNeighbourForOutputSignal(blockPos, newState.getBlock()); } } @Override public void updateNeighbors( BlockPos blockPos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState, int recursionLimit ) { Level level = getLevel(); // a == updateNeighbors // b == updateDiagonalNeighbors oldState.updateIndirectNeighbourShapes(level, blockPos, NOTIFY, recursionLimit); if (sideEffectSet.shouldApply(SideEffect.EVENTS)) { CraftWorld craftWorld = level.getWorld(); if (craftWorld != null) { BlockPhysicsEvent event = new BlockPhysicsEvent( craftWorld.getBlockAt(blockPos.getX(), blockPos.getY(), blockPos.getZ()), CraftBlockData.fromData(newState) ); level.getCraftServer().getPluginManager().callEvent(event); if (event.isCancelled()) { return; } } } newState.triggerEvent(level, blockPos, NOTIFY, recursionLimit); newState.updateIndirectNeighbourShapes(level, blockPos, NOTIFY, recursionLimit); } @Override public void onBlockStateChange( BlockPos blockPos, net.minecraft.world.level.block.state.BlockState oldState, net.minecraft.world.level.block.state.BlockState newState ) { getLevel().onBlockStateChange(blockPos, oldState, newState); } private synchronized void flushAsync(final boolean sendChunks) { final Set changes = Set.copyOf(cachedChanges); cachedChanges.clear(); final Set toSend; if (sendChunks) { toSend = Set.copyOf(cachedChunksToSend); cachedChunksToSend.clear(); } else { toSend = Collections.emptySet(); } RunnableVal runnableVal = new RunnableVal<>() { @Override public void run(Object value) { changes.forEach(cc -> cc.levelChunk.setBlockState(cc.blockPos, cc.blockState, sideEffectSet != null && sideEffectSet.shouldApply(SideEffect.UPDATE) )); if (!sendChunks) { return; } for (IntPair chunk : toSend) { PaperweightPlatformAdapter.sendChunk(getLevel().getWorld().getHandle(), chunk.x(), chunk.z(), false); } } }; TaskManager.taskManager().async(() -> TaskManager.taskManager().sync(runnableVal)); } @Override public synchronized void flush() { RunnableVal runnableVal = new RunnableVal<>() { @Override public void run(Object value) { cachedChanges.forEach(cc -> cc.levelChunk.setBlockState(cc.blockPos, cc.blockState, sideEffectSet != null && sideEffectSet.shouldApply(SideEffect.UPDATE) )); for (IntPair chunk : cachedChunksToSend) { PaperweightPlatformAdapter.sendChunk(getLevel().getWorld().getHandle(), chunk.x(), chunk.z(), false); } } }; if (Fawe.isMainThread()) { runnableVal.run(); } else { TaskManager.taskManager().sync(runnableVal); } cachedChanges.clear(); cachedChunksToSend.clear(); } private record CachedChange( LevelChunk levelChunk, BlockPos blockPos, net.minecraft.world.level.block.state.BlockState blockState ) { } }