001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.bukkit.queue;
020
021import com.google.inject.Inject;
022import com.plotsquared.bukkit.schematic.StateWrapper;
023import com.plotsquared.bukkit.util.BukkitBlockUtil;
024import com.plotsquared.core.configuration.Settings;
025import com.plotsquared.core.inject.factory.ChunkCoordinatorBuilderFactory;
026import com.plotsquared.core.inject.factory.ChunkCoordinatorFactory;
027import com.plotsquared.core.queue.BasicQueueCoordinator;
028import com.plotsquared.core.queue.ChunkCoordinator;
029import com.plotsquared.core.queue.LocalChunk;
030import com.plotsquared.core.util.ChunkUtil;
031import com.sk89q.jnbt.CompoundTag;
032import com.sk89q.worldedit.WorldEditException;
033import com.sk89q.worldedit.bukkit.BukkitAdapter;
034import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
035import com.sk89q.worldedit.extent.clipboard.Clipboard;
036import com.sk89q.worldedit.math.BlockVector2;
037import com.sk89q.worldedit.math.BlockVector3;
038import com.sk89q.worldedit.regions.CuboidRegion;
039import com.sk89q.worldedit.regions.Region;
040import com.sk89q.worldedit.util.SideEffect;
041import com.sk89q.worldedit.util.SideEffectSet;
042import com.sk89q.worldedit.world.World;
043import com.sk89q.worldedit.world.biome.BiomeType;
044import com.sk89q.worldedit.world.block.BaseBlock;
045import com.sk89q.worldedit.world.block.BlockState;
046import org.bukkit.Bukkit;
047import org.bukkit.Chunk;
048import org.bukkit.block.Block;
049import org.bukkit.block.Container;
050import org.bukkit.block.data.BlockData;
051import org.checkerframework.checker.nullness.qual.NonNull;
052
053import java.util.ArrayList;
054import java.util.Collection;
055import java.util.function.Consumer;
056
057public class BukkitQueueCoordinator extends BasicQueueCoordinator {
058
059    private static final SideEffectSet NO_SIDE_EFFECT_SET;
060    private static final SideEffectSet EDGE_SIDE_EFFECT_SET;
061    private static final SideEffectSet LIGHTING_SIDE_EFFECT_SET;
062    private static final SideEffectSet EDGE_LIGHTING_SIDE_EFFECT_SET;
063
064    static {
065        NO_SIDE_EFFECT_SET = SideEffectSet.none().with(SideEffect.LIGHTING, SideEffect.State.OFF).with(
066                SideEffect.NEIGHBORS,
067                SideEffect.State.OFF
068        );
069        EDGE_SIDE_EFFECT_SET = SideEffectSet.none().with(SideEffect.UPDATE, SideEffect.State.ON).with(
070                SideEffect.NEIGHBORS,
071                SideEffect.State.ON
072        );
073        LIGHTING_SIDE_EFFECT_SET = SideEffectSet.none().with(SideEffect.NEIGHBORS, SideEffect.State.OFF);
074        EDGE_LIGHTING_SIDE_EFFECT_SET = SideEffectSet.none().with(SideEffect.UPDATE, SideEffect.State.ON).with(
075                SideEffect.NEIGHBORS,
076                SideEffect.State.ON
077        );
078    }
079
080    private org.bukkit.World bukkitWorld;
081    @Inject
082    private ChunkCoordinatorBuilderFactory chunkCoordinatorBuilderFactory;
083    @Inject
084    private ChunkCoordinatorFactory chunkCoordinatorFactory;
085    private ChunkCoordinator chunkCoordinator;
086
087    @Inject
088    public BukkitQueueCoordinator(@NonNull World world) {
089        super(world);
090    }
091
092    @Override
093    public BlockState getBlock(int x, int y, int z) {
094        Block block = getBukkitWorld().getBlockAt(x, y, z);
095        return BukkitBlockUtil.get(block);
096    }
097
098    @Override
099    public void start() {
100        chunkCoordinator.start();
101    }
102
103    @Override
104    public void cancel() {
105        chunkCoordinator.cancel();
106    }
107
108    @Override
109    public boolean enqueue() {
110        final Clipboard regenClipboard;
111        if (isRegen()) {
112            BlockVector3 start = BlockVector3.at(getRegenStart()[0] << 4, getMinY(), getRegenStart()[1] << 4);
113            BlockVector3 end = BlockVector3.at((getRegenEnd()[0] << 4) + 15, getMaxY(), (getRegenEnd()[1] << 4) + 15);
114            Region region = new CuboidRegion(start, end);
115            regenClipboard = new BlockArrayClipboard(region);
116            regenClipboard.setOrigin(start);
117            getWorld().regenerate(region, regenClipboard);
118        } else if (getRegenRegion() != null) {
119            regenClipboard = new BlockArrayClipboard(getRegenRegion());
120            regenClipboard.setOrigin(getRegenRegion().getMinimumPoint());
121            getWorld().regenerate(getRegenRegion(), regenClipboard);
122        } else {
123            regenClipboard = null;
124        }
125        Consumer<BlockVector2> consumer = getChunkConsumer();
126        if (consumer == null) {
127            consumer = blockVector2 -> {
128                LocalChunk localChunk = getBlockChunks().get(blockVector2);
129                boolean isRegenChunk =
130                        regenClipboard != null && blockVector2.getBlockX() > getRegenStart()[0] && blockVector2.getBlockZ() > getRegenStart()[1]
131                                && blockVector2.getBlockX() < getRegenEnd()[0] && blockVector2.getBlockZ() < getRegenEnd()[1];
132                int sx = blockVector2.getX() << 4;
133                int sz = blockVector2.getZ() << 4;
134                if (isRegenChunk) {
135                    for (int layer = getMinLayer(); layer <= getMaxLayer(); layer++) {
136                        for (int y = 0; y < 16; y++) {
137                            for (int x = 0; x < 16; x++) {
138                                for (int z = 0; z < 16; z++) {
139                                    x += sx;
140                                    y += layer << 4;
141                                    z += sz;
142                                    BaseBlock block = regenClipboard.getFullBlock(BlockVector3.at(x, y, z));
143                                    if (block != null) {
144                                        boolean edge = Settings.QUEUE.UPDATE_EDGES && isEdgeRegen(x & 15, z & 15, blockVector2);
145                                        setWorldBlock(x, y, z, block, blockVector2, edge);
146                                    }
147                                }
148                            }
149                        }
150                    }
151                }
152                // Allow regen and then blocks to be placed (plot schematic etc)
153                if (localChunk == null) {
154                    return;
155                }
156                for (int layer = 0; layer < localChunk.getBaseblocks().length; layer++) {
157                    BaseBlock[] blocksLayer = localChunk.getBaseblocks()[layer];
158                    if (blocksLayer == null) {
159                        continue;
160                    }
161                    for (int j = 0; j < blocksLayer.length; j++) {
162                        if (blocksLayer[j] == null) {
163                            continue;
164                        }
165                        BaseBlock block = blocksLayer[j];
166
167                        if (block != null) {
168                            int lx = ChunkUtil.getX(j);
169                            int lz = ChunkUtil.getZ(j);
170                            int x = sx + lx;
171                            int y = ChunkUtil.getY(layer + localChunk.getMinSection(), j);
172                            int z = sz + lz;
173                            boolean edge = Settings.QUEUE.UPDATE_EDGES && isEdge(y >> 4, lx, y & 15, lz, blockVector2,
174                                    localChunk
175                            );
176                            setWorldBlock(x, y, z, block, blockVector2, edge);
177                        }
178                    }
179                }
180                for (int layer = 0; layer < localChunk.getBiomes().length; layer++) {
181                    BiomeType[] biomesLayer = localChunk.getBiomes()[layer];
182                    if (biomesLayer == null) {
183                        continue;
184                    }
185                    for (int j = 0; j < biomesLayer.length; j++) {
186                        if (biomesLayer[j] == null) {
187                            continue;
188                        }
189                        BiomeType biome = biomesLayer[j];
190                        if (biome != null) {
191                            int x = sx + ChunkUtil.getX(j);
192                            int y = ChunkUtil.getY(layer, j);
193                            int z = sz + ChunkUtil.getZ(j);
194                            getWorld().setBiome(BlockVector3.at(x, y, z), biome);
195                        }
196                    }
197                }
198                if (localChunk.getTiles().size() > 0) {
199                    localChunk.getTiles().forEach((blockVector3, tag) -> {
200                        try {
201                            BaseBlock block = getWorld().getBlock(blockVector3).toBaseBlock(tag);
202                            getWorld().setBlock(blockVector3, block, getSideEffectSet(SideEffectState.NONE));
203                        } catch (WorldEditException ignored) {
204                            StateWrapper sw = new StateWrapper(tag);
205                            sw.restoreTag(getWorld().getName(), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ());
206                        }
207                    });
208                }
209                if (localChunk.getEntities().size() > 0) {
210                    localChunk.getEntities().forEach((location, entity) -> getWorld().createEntity(location, entity));
211                }
212            };
213        }
214        Collection<BlockVector2> read = new ArrayList<>();
215        if (getReadChunks().size() > 0) {
216            read.addAll(getReadChunks());
217        }
218        chunkCoordinator =
219                chunkCoordinatorBuilderFactory
220                        .create(chunkCoordinatorFactory)
221                        .inWorld(getWorld())
222                        .withChunks(getBlockChunks().keySet())
223                        .withChunks(read)
224                        .withInitialBatchSize(3)
225                        .withMaxIterationTime(40)
226                        .withThrowableConsumer(Throwable::printStackTrace)
227                        .withFinalAction(getCompleteTask())
228                        .withConsumer(consumer)
229                        .unloadAfter(isUnloadAfter())
230                        .withProgressSubscribers(getProgressSubscribers())
231                        .forceSync(isForceSync())
232                        .build();
233        return super.enqueue();
234    }
235
236    /**
237     * Set a block to the world. First tries WNA but defaults to normal block setting methods if that fails
238     */
239    @SuppressWarnings("unused")
240    private void setWorldBlock(int x, int y, int z, @NonNull BaseBlock block, @NonNull BlockVector2 blockVector2, boolean edge) {
241        try {
242            BlockVector3 loc = BlockVector3.at(x, y, z);
243            boolean lighting = false;
244            switch (getLightingMode()) {
245                case NONE:
246                    break;
247                case PLACEMENT:
248                    lighting = block.getBlockType().getMaterial().getLightValue() > 0;
249                    break;
250                case REPLACEMENT:
251                    lighting = block.getBlockType().getMaterial().getLightValue() > 0
252                            || getWorld().getBlock(loc).getBlockType().getMaterial().getLightValue() > 0;
253                    break;
254                default:
255                    // Can only be "all"
256                    lighting = true;
257            }
258            SideEffectSet sideEffectSet;
259            if (lighting) {
260                sideEffectSet = getSideEffectSet(edge ? SideEffectState.EDGE_LIGHTING : SideEffectState.LIGHTING);
261            } else {
262                sideEffectSet = getSideEffectSet(edge ? SideEffectState.EDGE : SideEffectState.NONE);
263            }
264            getWorld().setBlock(loc, block, sideEffectSet);
265        } catch (WorldEditException ignored) {
266            // Fallback to not so nice method
267            BlockData blockData = BukkitAdapter.adapt(block);
268            Block existing;
269            // Assume a chunk object has been given only when it should have been.
270            if (getChunkObject() instanceof Chunk chunkObject) {
271                existing = chunkObject.getBlock(x & 15, y, z & 15);
272            } else {
273                 existing = getBukkitWorld().getBlockAt(x, y, z);
274            }
275            final BlockState existingBaseBlock = BukkitAdapter.adapt(existing.getBlockData());
276            if (BukkitBlockUtil.get(existing).equals(existingBaseBlock) && existing.getBlockData().matches(blockData)) {
277                return;
278            }
279
280            if (existing.getState() instanceof Container) {
281                ((Container) existing.getState()).getInventory().clear();
282            }
283
284            existing.setType(BukkitAdapter.adapt(block.getBlockType()), false);
285            existing.setBlockData(blockData, false);
286            if (block.hasNbtData()) {
287                CompoundTag tag = block.getNbtData();
288                StateWrapper sw = new StateWrapper(tag);
289
290                sw.restoreTag(existing);
291            }
292        }
293    }
294
295    private org.bukkit.World getBukkitWorld() {
296        if (bukkitWorld == null) {
297            bukkitWorld = Bukkit.getWorld(getWorld().getName());
298        }
299        return bukkitWorld;
300    }
301
302    private boolean isEdge(int layer, int x, int y, int z, BlockVector2 blockVector2, LocalChunk localChunk) {
303        int layerIndex = (layer - localChunk.getMinSection());
304        if (layer == localChunk.getMinSection() || layerIndex == localChunk.getBaseblocks().length - 1) {
305            return false;
306        }
307        if (x == 0) {
308            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() - 1));
309            if (localChunkX == null || localChunkX.getBaseblocks()[layerIndex] == null ||
310                    localChunkX.getBaseblocks()[layerIndex][ChunkUtil.getJ(15, y, z)] != null) {
311                return true;
312            }
313        } else if (x == 15) {
314            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() + 1));
315            if (localChunkX == null || localChunkX.getBaseblocks()[layerIndex] == null ||
316                    localChunkX.getBaseblocks()[layerIndex][ChunkUtil.getJ(0, y, z)] != null) {
317                return true;
318            }
319        }
320        if (z == 0) {
321            LocalChunk localChunkZ = getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() - 1));
322            if (localChunkZ == null || localChunkZ.getBaseblocks()[layerIndex] == null ||
323                    localChunkZ.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, y, 15)] != null) {
324                return true;
325            }
326        } else if (z == 15) {
327            LocalChunk localChunkZ = getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() + 1));
328            if (localChunkZ == null || localChunkZ.getBaseblocks()[layerIndex] == null ||
329                    localChunkZ.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, y, 0)] != null) {
330                return true;
331            }
332        }
333        if (y == 0) {
334            if (localChunk.getBaseblocks()[layerIndex - 1] == null ||
335                    localChunk.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, 15, z)] != null) {
336                return true;
337            }
338        } else if (y == 15) {
339            if (localChunk.getBaseblocks()[layerIndex + 1] == null ||
340                    localChunk.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, 0, z)] != null) {
341                return true;
342            }
343        }
344        BaseBlock[] baseBlocks = localChunk.getBaseblocks()[layerIndex];
345        if (x > 0 && baseBlocks[ChunkUtil.getJ(x - 1, y, z)] == null) {
346            return true;
347        }
348        if (x < 15 && baseBlocks[ChunkUtil.getJ(x + 1, y, z)] == null) {
349            return true;
350        }
351        if (y > 0 && baseBlocks[ChunkUtil.getJ(x, y - 1, z)] == null) {
352            return true;
353        }
354        if (y < 15 && baseBlocks[ChunkUtil.getJ(x, y + 1, z)] == null) {
355            return true;
356        }
357        if (z > 0 && baseBlocks[ChunkUtil.getJ(x, y, z - 1)] == null) {
358            return true;
359        }
360        return z < 15 && baseBlocks[ChunkUtil.getJ(x, y, z + 1)] == null;
361    }
362
363    private boolean isEdgeRegen(int x, int z, BlockVector2 blockVector2) {
364        if (x == 0) {
365            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() - 1));
366            if (localChunkX == null) {
367                return true;
368            }
369        } else if (x == 15) {
370            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() + 1));
371            if (localChunkX == null) {
372                return true;
373            }
374        }
375        if (z == 0) {
376            return getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() - 1)) == null;
377        } else if (z == 15) {
378            return getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() + 1)) == null;
379        }
380        return false;
381    }
382
383    private SideEffectSet getSideEffectSet(SideEffectState state) {
384        if (getSideEffectSet() != null) {
385            return getSideEffectSet();
386        }
387        return switch (state) {
388            case NONE -> NO_SIDE_EFFECT_SET;
389            case EDGE -> EDGE_SIDE_EFFECT_SET;
390            case LIGHTING -> LIGHTING_SIDE_EFFECT_SET;
391            case EDGE_LIGHTING -> EDGE_LIGHTING_SIDE_EFFECT_SET;
392        };
393    }
394
395    private enum SideEffectState {
396        NONE,
397        EDGE,
398        LIGHTING,
399        EDGE_LIGHTING
400    }
401
402}