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 = enableNetworkIfNeeded()
066                .with(SideEffect.LIGHTING, SideEffect.State.OFF)
067                .with(SideEffect.NEIGHBORS, SideEffect.State.OFF);
068        EDGE_SIDE_EFFECT_SET = NO_SIDE_EFFECT_SET
069                .with(SideEffect.UPDATE, SideEffect.State.ON)
070                .with(SideEffect.NEIGHBORS, SideEffect.State.ON);
071        LIGHTING_SIDE_EFFECT_SET = NO_SIDE_EFFECT_SET
072                .with(SideEffect.NEIGHBORS, SideEffect.State.OFF);
073        EDGE_LIGHTING_SIDE_EFFECT_SET = NO_SIDE_EFFECT_SET
074                .with(SideEffect.UPDATE, SideEffect.State.ON)
075                .with(SideEffect.NEIGHBORS, SideEffect.State.ON);
076    }
077
078    // make sure block changes are sent
079    private static SideEffectSet enableNetworkIfNeeded() {
080        SideEffect network;
081        try {
082            network = SideEffect.valueOf("NETWORK");
083        } catch (IllegalArgumentException ignored) {
084            return SideEffectSet.none();
085        }
086        return SideEffectSet.none().with(network, SideEffect.State.ON);
087    }
088
089    private org.bukkit.World bukkitWorld;
090    @Inject
091    private ChunkCoordinatorBuilderFactory chunkCoordinatorBuilderFactory;
092    @Inject
093    private ChunkCoordinatorFactory chunkCoordinatorFactory;
094    private ChunkCoordinator chunkCoordinator;
095
096    @Inject
097    public BukkitQueueCoordinator(@NonNull World world) {
098        super(world);
099    }
100
101    @Override
102    public BlockState getBlock(int x, int y, int z) {
103        Block block = getBukkitWorld().getBlockAt(x, y, z);
104        return BukkitBlockUtil.get(block);
105    }
106
107    @Override
108    public void start() {
109        chunkCoordinator.start();
110    }
111
112    @Override
113    public void cancel() {
114        chunkCoordinator.cancel();
115    }
116
117    @Override
118    public boolean enqueue() {
119        final Clipboard regenClipboard;
120        if (isRegen()) {
121            BlockVector3 start = BlockVector3.at(getRegenStart()[0] << 4, getMinY(), getRegenStart()[1] << 4);
122            BlockVector3 end = BlockVector3.at((getRegenEnd()[0] << 4) + 15, getMaxY(), (getRegenEnd()[1] << 4) + 15);
123            Region region = new CuboidRegion(start, end);
124            regenClipboard = new BlockArrayClipboard(region);
125            regenClipboard.setOrigin(start);
126            getWorld().regenerate(region, regenClipboard);
127        } else if (getRegenRegion() != null) {
128            regenClipboard = new BlockArrayClipboard(getRegenRegion());
129            regenClipboard.setOrigin(getRegenRegion().getMinimumPoint());
130            getWorld().regenerate(getRegenRegion(), regenClipboard);
131        } else {
132            regenClipboard = null;
133        }
134        Consumer<BlockVector2> consumer = getChunkConsumer();
135        if (consumer == null) {
136            consumer = blockVector2 -> {
137                LocalChunk localChunk = getBlockChunks().get(blockVector2);
138                boolean isRegenChunk =
139                        regenClipboard != null && blockVector2.getBlockX() > getRegenStart()[0] && blockVector2.getBlockZ() > getRegenStart()[1]
140                                && blockVector2.getBlockX() < getRegenEnd()[0] && blockVector2.getBlockZ() < getRegenEnd()[1];
141                int sx = blockVector2.getX() << 4;
142                int sz = blockVector2.getZ() << 4;
143                if (isRegenChunk) {
144                    for (int layer = getMinLayer(); layer <= getMaxLayer(); layer++) {
145                        for (int y = 0; y < 16; y++) {
146                            for (int x = 0; x < 16; x++) {
147                                for (int z = 0; z < 16; z++) {
148                                    x += sx;
149                                    y += layer << 4;
150                                    z += sz;
151                                    BaseBlock block = regenClipboard.getFullBlock(BlockVector3.at(x, y, z));
152                                    if (block != null) {
153                                        boolean edge = Settings.QUEUE.UPDATE_EDGES && isEdgeRegen(x & 15, z & 15, blockVector2);
154                                        setWorldBlock(x, y, z, block, blockVector2, edge);
155                                    }
156                                }
157                            }
158                        }
159                    }
160                }
161                // Allow regen and then blocks to be placed (plot schematic etc)
162                if (localChunk == null) {
163                    return;
164                }
165                for (int layer = 0; layer < localChunk.getBaseblocks().length; layer++) {
166                    BaseBlock[] blocksLayer = localChunk.getBaseblocks()[layer];
167                    if (blocksLayer == null) {
168                        continue;
169                    }
170                    for (int j = 0; j < blocksLayer.length; j++) {
171                        if (blocksLayer[j] == null) {
172                            continue;
173                        }
174                        BaseBlock block = blocksLayer[j];
175
176                        if (block != null) {
177                            int lx = ChunkUtil.getX(j);
178                            int lz = ChunkUtil.getZ(j);
179                            int x = sx + lx;
180                            int y = ChunkUtil.getY(layer + localChunk.getMinSection(), j);
181                            int z = sz + lz;
182                            boolean edge = Settings.QUEUE.UPDATE_EDGES && isEdge(y >> 4, lx, y & 15, lz, blockVector2,
183                                    localChunk
184                            );
185                            setWorldBlock(x, y, z, block, blockVector2, edge);
186                        }
187                    }
188                }
189                for (int layer = 0; layer < localChunk.getBiomes().length; layer++) {
190                    BiomeType[] biomesLayer = localChunk.getBiomes()[layer];
191                    if (biomesLayer == null) {
192                        continue;
193                    }
194                    for (int j = 0; j < biomesLayer.length; j++) {
195                        if (biomesLayer[j] == null) {
196                            continue;
197                        }
198                        BiomeType biome = biomesLayer[j];
199                        if (biome != null) {
200                            int x = sx + ChunkUtil.getX(j);
201                            int y = ChunkUtil.getY(layer, j);
202                            int z = sz + ChunkUtil.getZ(j);
203                            getWorld().setBiome(BlockVector3.at(x, y, z), biome);
204                        }
205                    }
206                }
207                if (localChunk.getTiles().size() > 0) {
208                    localChunk.getTiles().forEach((blockVector3, tag) -> {
209                        try {
210                            BaseBlock block = getWorld().getBlock(blockVector3).toBaseBlock(tag);
211                            getWorld().setBlock(blockVector3, block, getSideEffectSet(SideEffectState.NONE));
212                        } catch (WorldEditException ignored) {
213                            StateWrapper sw = new StateWrapper(tag);
214                            sw.restoreTag(getWorld().getName(), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ());
215                        }
216                    });
217                }
218                if (localChunk.getEntities().size() > 0) {
219                    localChunk.getEntities().forEach((location, entity) -> getWorld().createEntity(location, entity));
220                }
221            };
222        }
223        Collection<BlockVector2> read = new ArrayList<>();
224        if (getReadChunks().size() > 0) {
225            read.addAll(getReadChunks());
226        }
227        chunkCoordinator =
228                chunkCoordinatorBuilderFactory
229                        .create(chunkCoordinatorFactory)
230                        .inWorld(getWorld())
231                        .withChunks(getBlockChunks().keySet())
232                        .withChunks(read)
233                        .withInitialBatchSize(3)
234                        .withMaxIterationTime(40)
235                        .withThrowableConsumer(Throwable::printStackTrace)
236                        .withFinalAction(getCompleteTask())
237                        .withConsumer(consumer)
238                        .unloadAfter(isUnloadAfter())
239                        .withProgressSubscribers(getProgressSubscribers())
240                        .forceSync(isForceSync())
241                        .shouldGen(isShouldGen())
242                        .build();
243        return super.enqueue();
244    }
245
246    /**
247     * Set a block to the world. First tries WNA but defaults to normal block setting methods if that fails
248     */
249    @SuppressWarnings("unused")
250    private void setWorldBlock(int x, int y, int z, @NonNull BaseBlock block, @NonNull BlockVector2 blockVector2, boolean edge) {
251        try {
252            BlockVector3 loc = BlockVector3.at(x, y, z);
253            boolean lighting = false;
254            switch (getLightingMode()) {
255                case NONE:
256                    break;
257                case PLACEMENT:
258                    lighting = block.getBlockType().getMaterial().getLightValue() > 0;
259                    break;
260                case REPLACEMENT:
261                    lighting = block.getBlockType().getMaterial().getLightValue() > 0
262                            || getWorld().getBlock(loc).getBlockType().getMaterial().getLightValue() > 0;
263                    break;
264                default:
265                    // Can only be "all"
266                    lighting = true;
267            }
268            SideEffectSet sideEffectSet;
269            if (lighting) {
270                sideEffectSet = getSideEffectSet(edge ? SideEffectState.EDGE_LIGHTING : SideEffectState.LIGHTING);
271            } else {
272                sideEffectSet = getSideEffectSet(edge ? SideEffectState.EDGE : SideEffectState.NONE);
273            }
274            getWorld().setBlock(loc, block, sideEffectSet);
275        } catch (WorldEditException ignored) {
276            // Fallback to not so nice method
277            BlockData blockData = BukkitAdapter.adapt(block);
278            Block existing;
279            // Assume a chunk object has been given only when it should have been.
280            if (getChunkObject() instanceof Chunk chunkObject) {
281                existing = chunkObject.getBlock(x & 15, y, z & 15);
282            } else {
283                existing = getBukkitWorld().getBlockAt(x, y, z);
284            }
285            final BlockState existingBaseBlock = BukkitAdapter.adapt(existing.getBlockData());
286            if (BukkitBlockUtil.get(existing).equals(existingBaseBlock) && existing.getBlockData().matches(blockData)) {
287                return;
288            }
289
290            if (existing.getState() instanceof Container) {
291                ((Container) existing.getState()).getInventory().clear();
292            }
293
294            existing.setType(BukkitAdapter.adapt(block.getBlockType()), false);
295            existing.setBlockData(blockData, false);
296            if (block.hasNbtData()) {
297                CompoundTag tag = block.getNbtData();
298                StateWrapper sw = new StateWrapper(tag);
299
300                sw.restoreTag(existing);
301            }
302        }
303    }
304
305    private org.bukkit.World getBukkitWorld() {
306        if (bukkitWorld == null) {
307            bukkitWorld = Bukkit.getWorld(getWorld().getName());
308        }
309        return bukkitWorld;
310    }
311
312    private boolean isEdge(int layer, int x, int y, int z, BlockVector2 blockVector2, LocalChunk localChunk) {
313        int layerIndex = (layer - localChunk.getMinSection());
314        if (layer == localChunk.getMinSection() || layerIndex == localChunk.getBaseblocks().length - 1) {
315            return false;
316        }
317        if (x == 0) {
318            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() - 1));
319            if (localChunkX == null || localChunkX.getBaseblocks()[layerIndex] == null ||
320                    localChunkX.getBaseblocks()[layerIndex][ChunkUtil.getJ(15, y, z)] != null) {
321                return true;
322            }
323        } else if (x == 15) {
324            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() + 1));
325            if (localChunkX == null || localChunkX.getBaseblocks()[layerIndex] == null ||
326                    localChunkX.getBaseblocks()[layerIndex][ChunkUtil.getJ(0, y, z)] != null) {
327                return true;
328            }
329        }
330        if (z == 0) {
331            LocalChunk localChunkZ = getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() - 1));
332            if (localChunkZ == null || localChunkZ.getBaseblocks()[layerIndex] == null ||
333                    localChunkZ.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, y, 15)] != null) {
334                return true;
335            }
336        } else if (z == 15) {
337            LocalChunk localChunkZ = getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() + 1));
338            if (localChunkZ == null || localChunkZ.getBaseblocks()[layerIndex] == null ||
339                    localChunkZ.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, y, 0)] != null) {
340                return true;
341            }
342        }
343        if (y == 0) {
344            if (localChunk.getBaseblocks()[layerIndex - 1] == null ||
345                    localChunk.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, 15, z)] != null) {
346                return true;
347            }
348        } else if (y == 15) {
349            if (localChunk.getBaseblocks()[layerIndex + 1] == null ||
350                    localChunk.getBaseblocks()[layerIndex][ChunkUtil.getJ(x, 0, z)] != null) {
351                return true;
352            }
353        }
354        BaseBlock[] baseBlocks = localChunk.getBaseblocks()[layerIndex];
355        if (x > 0 && baseBlocks[ChunkUtil.getJ(x - 1, y, z)] == null) {
356            return true;
357        }
358        if (x < 15 && baseBlocks[ChunkUtil.getJ(x + 1, y, z)] == null) {
359            return true;
360        }
361        if (y > 0 && baseBlocks[ChunkUtil.getJ(x, y - 1, z)] == null) {
362            return true;
363        }
364        if (y < 15 && baseBlocks[ChunkUtil.getJ(x, y + 1, z)] == null) {
365            return true;
366        }
367        if (z > 0 && baseBlocks[ChunkUtil.getJ(x, y, z - 1)] == null) {
368            return true;
369        }
370        return z < 15 && baseBlocks[ChunkUtil.getJ(x, y, z + 1)] == null;
371    }
372
373    private boolean isEdgeRegen(int x, int z, BlockVector2 blockVector2) {
374        if (x == 0) {
375            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() - 1));
376            if (localChunkX == null) {
377                return true;
378            }
379        } else if (x == 15) {
380            LocalChunk localChunkX = getBlockChunks().get(blockVector2.withX(blockVector2.getX() + 1));
381            if (localChunkX == null) {
382                return true;
383            }
384        }
385        if (z == 0) {
386            return getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() - 1)) == null;
387        } else if (z == 15) {
388            return getBlockChunks().get(blockVector2.withZ(blockVector2.getZ() + 1)) == null;
389        }
390        return false;
391    }
392
393    private SideEffectSet getSideEffectSet(SideEffectState state) {
394        if (getSideEffectSet() != null) {
395            return getSideEffectSet();
396        }
397        return switch (state) {
398            case NONE -> NO_SIDE_EFFECT_SET;
399            case EDGE -> EDGE_SIDE_EFFECT_SET;
400            case LIGHTING -> LIGHTING_SIDE_EFFECT_SET;
401            case EDGE_LIGHTING -> EDGE_LIGHTING_SIDE_EFFECT_SET;
402        };
403    }
404
405    private enum SideEffectState {
406        NONE,
407        EDGE,
408        LIGHTING,
409        EDGE_LIGHTING
410    }
411
412}