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.core.generator;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.configuration.Settings;
024import com.plotsquared.core.events.PlotFlagAddEvent;
025import com.plotsquared.core.events.Result;
026import com.plotsquared.core.listener.WEExtent;
027import com.plotsquared.core.location.Location;
028import com.plotsquared.core.plot.Plot;
029import com.plotsquared.core.plot.PlotArea;
030import com.plotsquared.core.plot.PlotAreaType;
031import com.plotsquared.core.plot.PlotId;
032import com.plotsquared.core.plot.PlotManager;
033import com.plotsquared.core.plot.expiration.PlotAnalysis;
034import com.plotsquared.core.plot.flag.GlobalFlagContainer;
035import com.plotsquared.core.plot.flag.PlotFlag;
036import com.plotsquared.core.plot.flag.implementations.AnalysisFlag;
037import com.plotsquared.core.plot.world.PlotAreaManager;
038import com.plotsquared.core.queue.BlockArrayCacheScopedQueueCoordinator;
039import com.plotsquared.core.queue.GlobalBlockQueue;
040import com.plotsquared.core.queue.QueueCoordinator;
041import com.plotsquared.core.util.ChunkManager;
042import com.plotsquared.core.util.EventDispatcher;
043import com.plotsquared.core.util.MathMan;
044import com.plotsquared.core.util.RegionManager;
045import com.plotsquared.core.util.RegionUtil;
046import com.plotsquared.core.util.SchematicHandler;
047import com.plotsquared.core.util.WorldUtil;
048import com.plotsquared.core.util.task.RunnableVal;
049import com.plotsquared.core.util.task.TaskManager;
050import com.plotsquared.core.util.task.TaskTime;
051import com.sk89q.worldedit.math.BlockVector2;
052import com.sk89q.worldedit.math.BlockVector3;
053import com.sk89q.worldedit.regions.CuboidRegion;
054import com.sk89q.worldedit.world.biome.BiomeType;
055import com.sk89q.worldedit.world.block.BaseBlock;
056import com.sk89q.worldedit.world.block.BlockState;
057import com.sk89q.worldedit.world.block.BlockType;
058import com.sk89q.worldedit.world.block.BlockTypes;
059import org.apache.logging.log4j.LogManager;
060import org.apache.logging.log4j.Logger;
061import org.checkerframework.checker.nullness.qual.NonNull;
062import org.checkerframework.checker.nullness.qual.Nullable;
063
064import java.io.File;
065import java.util.ArrayDeque;
066import java.util.ArrayList;
067import java.util.Collections;
068import java.util.HashSet;
069import java.util.Iterator;
070import java.util.LinkedHashSet;
071import java.util.List;
072import java.util.Objects;
073import java.util.Set;
074import java.util.concurrent.atomic.AtomicBoolean;
075import java.util.concurrent.atomic.AtomicInteger;
076
077public class HybridUtils {
078
079    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + HybridUtils.class.getSimpleName());
080    private static final BlockState AIR = BlockTypes.AIR.getDefaultState();
081
082    /**
083     * Deprecated and likely to be removed in a future release.
084     */
085    @Deprecated(forRemoval = true, since = "7.0.0")
086    public static HybridUtils manager;
087    public static Set<BlockVector2> regions;
088    public static int height;
089    // Use ordered for reasonable chunk loading order to reduce paper unloading neighbour chunks and then us attempting to load
090    // them again, causing errors
091    public static Set<BlockVector2> chunks = new LinkedHashSet<>();
092    public static PlotArea area;
093    public static boolean UPDATE = false;
094
095    private final PlotAreaManager plotAreaManager;
096    private final ChunkManager chunkManager;
097    private final GlobalBlockQueue blockQueue;
098    private final WorldUtil worldUtil;
099    private final SchematicHandler schematicHandler;
100    private final EventDispatcher eventDispatcher;
101
102    @Inject
103    public HybridUtils(
104            final @NonNull PlotAreaManager plotAreaManager,
105            final @NonNull ChunkManager chunkManager,
106            final @NonNull GlobalBlockQueue blockQueue,
107            final @NonNull WorldUtil worldUtil,
108            final @NonNull SchematicHandler schematicHandler,
109            final @NonNull EventDispatcher eventDispatcher
110    ) {
111        this.plotAreaManager = plotAreaManager;
112        this.chunkManager = chunkManager;
113        this.blockQueue = blockQueue;
114        this.worldUtil = worldUtil;
115        this.schematicHandler = schematicHandler;
116        this.eventDispatcher = eventDispatcher;
117    }
118
119    public void regeneratePlotWalls(final PlotArea area) {
120        PlotManager plotManager = area.getPlotManager();
121        plotManager.regenerateAllPlotWalls(null);
122    }
123
124    public void analyzeRegion(final String world, final CuboidRegion region, final RunnableVal<PlotAnalysis> whenDone) {
125        // int diff, int variety, int vertices, int rotation, int height_sd
126        /*
127         * diff: compare to base by looping through all blocks
128         * variety: add to HashSet for each BlockState
129         * height_sd: loop over all blocks and get top block
130         *
131         * vertices: store air map and compare with neighbours
132         * for each block check the adjacent
133         *  - Store all blocks then go through in second loop
134         *  - recheck each block
135         *
136         */
137        TaskManager.runTaskAsync(() -> {
138            final PlotArea area = this.plotAreaManager.getPlotArea(world, null);
139            if (!(area instanceof HybridPlotWorld hpw)) {
140                return;
141            }
142
143            final BlockVector3 bot = region.getMinimumPoint();
144            final BlockVector3 top = region.getMaximumPoint();
145
146            final int bx = bot.getX();
147            final int bz = bot.getZ();
148            final int tx = top.getX();
149            final int tz = top.getZ();
150            final int cbx = bx >> 4;
151            final int cbz = bz >> 4;
152            final int ctx = tx >> 4;
153            final int ctz = tz >> 4;
154            final int width = tx - bx + 1;
155            final int length = tz - bz + 1;
156            final int height = area.getMaxGenHeight() - area.getMinGenHeight() + 1;
157            final int minHeight = area.getMinGenHeight();
158
159            final BlockState[][][] newBlocks = new BlockState[height][width][length];
160
161            BlockArrayCacheScopedQueueCoordinator oldBlockQueue = new BlockArrayCacheScopedQueueCoordinator(
162                    Location.at("", region.getMinimumPoint().withY(hpw.getMinGenHeight())),
163                    Location.at("", region.getMaximumPoint().withY(hpw.getMaxGenHeight()))
164            );
165
166            region.getChunks().forEach(chunkPos -> {
167                int relChunkX = chunkPos.getX() - cbx;
168                int relChunkZ = chunkPos.getZ() - cbz;
169                oldBlockQueue.setOffsetX(relChunkX << 4);
170                oldBlockQueue.setOffsetZ(relChunkZ << 4);
171                hpw.getGenerator().generateChunk(oldBlockQueue, hpw, false);
172            });
173
174            final BlockState[][][] oldBlocks = oldBlockQueue.getBlockStates();
175
176            QueueCoordinator queue = area.getQueue();
177            queue.addReadChunks(region.getChunks());
178            queue.setChunkConsumer(chunkPos -> {
179                int X = chunkPos.getX();
180                int Z = chunkPos.getZ();
181                int minX;
182                if (X == cbx) {
183                    minX = bx & 15;
184                } else {
185                    minX = 0;
186                }
187                int minZ;
188                if (Z == cbz) {
189                    minZ = bz & 15;
190                } else {
191                    minZ = 0;
192                }
193                int maxX;
194                if (X == ctx) {
195                    maxX = tx & 15;
196                } else {
197                    maxX = 15;
198                }
199                int maxZ;
200                if (Z == ctz) {
201                    maxZ = tz & 15;
202                } else {
203                    maxZ = 15;
204                }
205
206                int chunkBlockX = X << 4;
207                int chunkBlockZ = Z << 4;
208
209                int xb = chunkBlockX - bx;
210                int zb = chunkBlockZ - bz;
211                for (int x = minX; x <= maxX; x++) {
212                    int xx = chunkBlockX + x;
213                    for (int z = minZ; z <= maxZ; z++) {
214                        int zz = chunkBlockZ + z;
215                        for (int yIndex = 0; yIndex < height; yIndex++) {
216                            int y = yIndex + minHeight;
217                            BlockState block = queue.getBlock(xx, y, zz);
218                            if (block == null) {
219                                block = AIR;
220                            }
221                            int xr = xb + x;
222                            int zr = zb + z;
223                            newBlocks[yIndex][xr][zr] = block;
224                        }
225                    }
226                }
227            });
228
229            final Runnable run = () -> {
230                int size = width * length;
231                int[] changes = new int[size];
232                int[] faces = new int[size];
233                int[] data = new int[size];
234                int[] air = new int[size];
235                int[] variety = new int[size];
236                int i = 0;
237                for (int x = 0; x < width; x++) {
238                    for (int z = 0; z < length; z++) {
239                        Set<BlockType> types = new HashSet<>();
240                        for (int yIndex = 0; yIndex < height; yIndex++) {
241                            BlockState old = oldBlocks[yIndex][x][z]; // Nullable
242                            BlockState now = newBlocks[yIndex][x][z]; // Not null
243                            if (now == null) {
244                                throw new NullPointerException(String.format(
245                                        "\"now\" block null attempting to perform plot analysis. Indexes: x=%d of %d, yIndex=%d" +
246                                                " of %d, z=%d of %d",
247                                        x,
248                                        width,
249                                        yIndex,
250                                        height,
251                                        z,
252                                        length
253                                ));
254                            }
255                            if (!now.equals(old) && !(old == null && now.getBlockType().equals(BlockTypes.AIR))) {
256                                changes[i]++;
257                            }
258                            if (now.getBlockType().getMaterial().isAir()) {
259                                air[i]++;
260                            } else {
261                                // check vertices
262                                // modifications_adjacent
263                                if (x > 0 && z > 0 && yIndex > 0 && x < width - 1 && z < length - 1 && yIndex < (height - 1)) {
264                                    if (newBlocks[yIndex - 1][x][z].getBlockType().getMaterial().isAir()) {
265                                        faces[i]++;
266                                    }
267                                    if (newBlocks[yIndex][x - 1][z].getBlockType().getMaterial().isAir()) {
268                                        faces[i]++;
269                                    }
270                                    if (newBlocks[yIndex][x][z - 1].getBlockType().getMaterial().isAir()) {
271                                        faces[i]++;
272                                    }
273                                    if (newBlocks[yIndex + 1][x][z].getBlockType().getMaterial().isAir()) {
274                                        faces[i]++;
275                                    }
276                                    if (newBlocks[yIndex][x + 1][z].getBlockType().getMaterial().isAir()) {
277                                        faces[i]++;
278                                    }
279                                    if (newBlocks[yIndex][x][z + 1].getBlockType().getMaterial().isAir()) {
280                                        faces[i]++;
281                                    }
282                                }
283
284                                if (!now.equals(now.getBlockType().getDefaultState())) {
285                                    data[i]++;
286                                }
287                                types.add(now.getBlockType());
288                            }
289                        }
290                        variety[i] = types.size();
291                        i++;
292                    }
293                }
294                // analyze plot
295                // put in analysis obj
296
297                // run whenDone
298                PlotAnalysis analysis = new PlotAnalysis();
299                analysis.changes = (int) (MathMan.getMean(changes) * 100);
300                analysis.faces = (int) (MathMan.getMean(faces) * 100);
301                analysis.data = (int) (MathMan.getMean(data) * 100);
302                analysis.air = (int) (MathMan.getMean(air) * 100);
303                analysis.variety = (int) (MathMan.getMean(variety) * 100);
304
305                analysis.changes_sd = (int) (MathMan.getSD(changes, analysis.changes) * 100);
306                analysis.faces_sd = (int) (MathMan.getSD(faces, analysis.faces) * 100);
307                analysis.data_sd = (int) (MathMan.getSD(data, analysis.data) * 100);
308                analysis.air_sd = (int) (MathMan.getSD(air, analysis.air) * 100);
309                analysis.variety_sd = (int) (MathMan.getSD(variety, analysis.variety) * 100);
310                whenDone.value = analysis;
311                whenDone.run();
312            };
313            queue.setCompleteTask(run);
314            queue.enqueue();
315        });
316    }
317
318    public void analyzePlot(final Plot origin, final RunnableVal<PlotAnalysis> whenDone) {
319        final ArrayDeque<CuboidRegion> zones = new ArrayDeque<>(origin.getRegions());
320        final ArrayList<PlotAnalysis> analysis = new ArrayList<>();
321        Runnable run = new Runnable() {
322            @Override
323            public void run() {
324                if (zones.isEmpty()) {
325                    if (!analysis.isEmpty()) {
326                        whenDone.value = new PlotAnalysis();
327                        for (PlotAnalysis data : analysis) {
328                            whenDone.value.air += data.air;
329                            whenDone.value.air_sd += data.air_sd;
330                            whenDone.value.changes += data.changes;
331                            whenDone.value.changes_sd += data.changes_sd;
332                            whenDone.value.data += data.data;
333                            whenDone.value.data_sd += data.data_sd;
334                            whenDone.value.faces += data.faces;
335                            whenDone.value.faces_sd += data.faces_sd;
336                            whenDone.value.variety += data.variety;
337                            whenDone.value.variety_sd += data.variety_sd;
338                        }
339                        whenDone.value.air /= analysis.size();
340                        whenDone.value.air_sd /= analysis.size();
341                        whenDone.value.changes /= analysis.size();
342                        whenDone.value.changes_sd /= analysis.size();
343                        whenDone.value.data /= analysis.size();
344                        whenDone.value.data_sd /= analysis.size();
345                        whenDone.value.faces /= analysis.size();
346                        whenDone.value.faces_sd /= analysis.size();
347                        whenDone.value.variety /= analysis.size();
348                        whenDone.value.variety_sd /= analysis.size();
349                    } else {
350                        whenDone.value = analysis.get(0);
351                    }
352                    List<Integer> result = new ArrayList<>();
353                    result.add(whenDone.value.changes);
354                    result.add(whenDone.value.faces);
355                    result.add(whenDone.value.data);
356                    result.add(whenDone.value.air);
357                    result.add(whenDone.value.variety);
358
359                    result.add(whenDone.value.changes_sd);
360                    result.add(whenDone.value.faces_sd);
361                    result.add(whenDone.value.data_sd);
362                    result.add(whenDone.value.air_sd);
363                    result.add(whenDone.value.variety_sd);
364                    PlotFlag<?, ?> plotFlag = GlobalFlagContainer.getInstance().getFlag(AnalysisFlag.class).createFlagInstance(
365                            result);
366                    PlotFlagAddEvent event = eventDispatcher.callFlagAdd(plotFlag, origin);
367                    if (event.getEventResult() == Result.DENY) {
368                        return;
369                    }
370                    origin.setFlag(event.getFlag());
371                    TaskManager.runTask(whenDone);
372                    return;
373                }
374                CuboidRegion region = zones.poll();
375                final Runnable task = this;
376                analyzeRegion(origin.getWorldName(), region, new RunnableVal<>() {
377                    @Override
378                    public void run(PlotAnalysis value) {
379                        analysis.add(value);
380                        TaskManager.runTaskLater(task, TaskTime.ticks(1L));
381                    }
382                });
383            }
384        };
385        run.run();
386    }
387
388    public final ArrayList<BlockVector2> getChunks(BlockVector2 region) {
389        ArrayList<BlockVector2> chunks = new ArrayList<>();
390        int sx = region.getX() << 5;
391        int sz = region.getZ() << 5;
392        for (int x = sx; x < sx + 32; x++) {
393            for (int z = sz; z < sz + 32; z++) {
394                chunks.add(BlockVector2.at(x, z));
395            }
396        }
397        return chunks;
398    }
399
400    public boolean scheduleRoadUpdate(PlotArea area, int extend) {
401        if (HybridUtils.UPDATE) {
402            return false;
403        }
404        HybridUtils.UPDATE = true;
405        Set<BlockVector2> regions = this.worldUtil.getChunkChunks(PlotSquared.platform().getPlatformWorld(area.getWorldName()));
406        return scheduleRoadUpdate(area, regions, extend, new LinkedHashSet<>());
407    }
408
409    public boolean scheduleSingleRegionRoadUpdate(Plot plot, int extend) {
410        if (HybridUtils.UPDATE) {
411            return false;
412        }
413        HybridUtils.UPDATE = true;
414        Set<BlockVector2> regions = new HashSet<>();
415        regions.add(RegionManager.getRegion(plot.getCenterSynchronous()));
416        return scheduleRoadUpdate(plot.getArea(), regions, extend, new LinkedHashSet<>());
417    }
418
419    public boolean scheduleRoadUpdate(
420            final PlotArea area,
421            Set<BlockVector2> regions,
422            final int extend,
423            Set<BlockVector2> chunks
424    ) {
425        HybridUtils.regions = regions;
426        HybridUtils.area = area;
427        HybridUtils.height = extend;
428        HybridUtils.chunks = chunks;
429        final int initial = 1024 * regions.size() + chunks.size();
430        final AtomicInteger count = new AtomicInteger(0);
431        TaskManager.runTask(new Runnable() {
432            @Override
433            public void run() {
434                if (!UPDATE) {
435                    Iterator<BlockVector2> iter = chunks.iterator();
436                    QueueCoordinator queue = blockQueue.getNewQueue(worldUtil.getWeWorld(area.getWorldName()));
437                    queue.setShouldGen(false);
438                    while (iter.hasNext()) {
439                        BlockVector2 chunk = iter.next();
440                        iter.remove();
441                        RoadRegenerateResult result = regenerateRoad(area, chunk, extend, queue);
442                        if (result != RoadRegenerateResult.SUCCESS) {
443                            LOGGER.info(
444                                    "Failed to regenerate roads in chunk {}: {}", chunk,
445                                    result.getMessage()
446                            );
447                            if (result.irrecoverable()) {
448                                LOGGER.error("Cancelling road regen");
449                                chunks.clear();
450                                queue.cancel();
451                                HybridUtils.regions.clear();
452                                return;
453                            }
454                        }
455                    }
456                    queue.enqueue();
457                    LOGGER.info("Cancelled road task");
458                    return;
459                }
460                count.incrementAndGet();
461                if (count.intValue() % 10 == 0) {
462                    LOGGER.info("Progress: {}%", 100 * (initial - (chunks.size() + 1024 * regions.size())) / initial);
463                }
464                if (HybridUtils.regions.isEmpty() && chunks.isEmpty()) {
465                    regeneratePlotWalls(area);
466
467                    HybridUtils.UPDATE = false;
468                    LOGGER.info("Finished road conversion");
469                    // CANCEL TASK
470                } else {
471                    final Runnable task = this;
472                    TaskManager.runTaskAsync(() -> {
473                        try {
474                            if (chunks.size() < 64) {
475                                if (!HybridUtils.regions.isEmpty()) {
476                                    Iterator<BlockVector2> iterator = HybridUtils.regions.iterator();
477                                    BlockVector2 loc = iterator.next();
478                                    iterator.remove();
479                                    LOGGER.info("Updating .mcr: {}, {} (approx 1024 chunks)", loc.getX(), loc.getZ());
480                                    LOGGER.info("- Remaining: {}", HybridUtils.regions.size());
481                                    chunks.addAll(getChunks(loc));
482                                    System.gc();
483                                }
484                            }
485                            if (!chunks.isEmpty()) {
486                                TaskManager.getPlatformImplementation().sync(() -> {
487                                    Iterator<BlockVector2> iterator = chunks.iterator();
488                                    if (chunks.size() >= 32) {
489                                        QueueCoordinator queue = blockQueue.getNewQueue(worldUtil.getWeWorld(area.getWorldName()));
490                                        queue.setShouldGen(false);
491                                        for (int i = 0; i < 32; i++) {
492                                            final BlockVector2 chunk = iterator.next();
493                                            iterator.remove();
494                                            RoadRegenerateResult result = regenerateRoad(area, chunk, extend, queue);
495                                            if (result != RoadRegenerateResult.SUCCESS) {
496                                                LOGGER.info(
497                                                        "Failed to regenerate the road in chunk {}: {}", chunk,
498                                                        result.getMessage()
499                                                );
500                                                if (result.irrecoverable()) {
501                                                    LOGGER.error("Cancelling road regen");
502                                                    chunks.clear();
503                                                    queue.cancel();
504                                                    HybridUtils.regions.clear();
505                                                    TaskManager.runTaskLater(task, TaskTime.seconds(1L));
506                                                    return null;
507                                                }
508                                            }
509                                        }
510                                        queue.setCompleteTask(task);
511                                        queue.enqueue();
512                                        return null;
513                                    }
514                                    QueueCoordinator queue = blockQueue.getNewQueue(worldUtil.getWeWorld(area.getWorldName()));
515                                    queue.setShouldGen(false);
516                                    while (!chunks.isEmpty()) {
517                                        final BlockVector2 chunk = iterator.next();
518                                        iterator.remove();
519                                        RoadRegenerateResult result = regenerateRoad(area, chunk, extend, queue);
520                                        if (result != RoadRegenerateResult.SUCCESS) {
521                                            LOGGER.info(
522                                                    "Failed to regenerate road in chunk {}: {}", chunk,
523                                                    result.getMessage()
524                                            );
525                                            if (result.irrecoverable()) {
526                                                LOGGER.error("Cancelling road regen");
527                                                chunks.clear();
528                                                queue.cancel();
529                                                HybridUtils.regions.clear();
530                                                TaskManager.runTaskLater(task, TaskTime.seconds(1L));
531                                                return null;
532                                            }
533                                        }
534                                    }
535                                    queue.setCompleteTask(task);
536                                    queue.enqueue();
537                                    return null;
538                                });
539                                return;
540                            }
541                        } catch (Exception e) {
542                            Iterator<BlockVector2> iterator = HybridUtils.regions.iterator();
543                            BlockVector2 loc = iterator.next();
544                            iterator.remove();
545                            LOGGER.error(
546                                    "Error! Could not update '{}/region/r.{}.{}.mca' (Corrupt chunk?)",
547                                    area.getWorldHash(),
548                                    loc.getX(),
549                                    loc.getZ(),
550                                    e
551                            );
552                        }
553                        TaskManager.runTaskLater(task, TaskTime.seconds(1L));
554                    });
555                }
556            }
557        });
558        return true;
559    }
560
561    public boolean setupRoadSchematic(Plot plot) {
562        final String world = plot.getWorldName();
563        final QueueCoordinator queue = blockQueue.getNewQueue(worldUtil.getWeWorld(world));
564        Location bot = plot.getBottomAbs().subtract(1, 0, 1);
565        Location top = plot.getTopAbs();
566        final HybridPlotWorld plotworld = Objects.requireNonNull((HybridPlotWorld) plot.getArea());
567        // Do not use plotworld#schematicStartHeight() here as we want to restore the pre 6.1.4 way of doing it if
568        //  USE_WALL_IN_ROAD_SCHEM_HEIGHT is false
569        int schemY = Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT ?
570                Math.min(plotworld.PLOT_HEIGHT, Math.min(plotworld.WALL_HEIGHT, plotworld.ROAD_HEIGHT)) : plotworld.ROAD_HEIGHT;
571        int sx = bot.getX() - plotworld.ROAD_WIDTH + 1;
572        int sz = bot.getZ() + 1;
573        int sy = Settings.Schematics.PASTE_ROAD_ON_TOP ? schemY : plot.getArea().getMinGenHeight();
574        int ex = bot.getX();
575        int ez = top.getZ();
576        int ey = get_ey(plotworld, queue, sx, ex, sz, ez, sy);
577        int bz = sz - plotworld.ROAD_WIDTH;
578        int tz = sz - 1;
579        int ty = get_ey(plotworld, queue, sx, ex, bz, tz, sy);
580
581        final Set<CuboidRegion> sideRoad = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez));
582        final Set<CuboidRegion> intersection = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ty, bz, tz));
583
584        final String dir = Settings.Paths.SCHEMATICS + File.separator + "GEN_ROAD_SCHEMATIC" + File.separator + plot
585                .getArea()
586                .toString() + File.separator;
587
588        this.schematicHandler.getCompoundTag(world, sideRoad)
589                .whenComplete((compoundTag, throwable) -> {
590                    schematicHandler.save(compoundTag, dir + "sideroad.schem");
591                    schematicHandler.getCompoundTag(world, intersection)
592                            .whenComplete((c, t) -> {
593                                schematicHandler.save(c, dir + "intersection.schem");
594                                plotworld.ROAD_SCHEMATIC_ENABLED = true;
595                                try {
596                                    plotworld.setupSchematics();
597                                } catch (SchematicHandler.UnsupportedFormatException e) {
598                                    LOGGER.error(e);
599                                }
600                            });
601                });
602        return true;
603    }
604
605    private int get_ey(final HybridPlotWorld hpw, QueueCoordinator queue, int sx, int ex, int sz, int ez, int sy) {
606        int ey = sy;
607        for (int x = sx; x <= ex; x++) {
608            for (int z = sz; z <= ez; z++) {
609                for (int y = sy; y <= hpw.getMaxGenHeight(); y++) {
610                    if (y > ey) {
611                        BlockState block = queue.getBlock(x, y, z);
612                        if (!block.getBlockType().getMaterial().isAir()) {
613                            ey = y;
614                        }
615                    }
616                }
617            }
618        }
619        return ey;
620    }
621
622    /**
623     * Regenerate the road in a chunk in a plot area.
624     *
625     * @param area             Plot area to regenerate road for
626     * @param chunk            Chunk location to regenerate
627     * @param extend           How far to extend setting air above the road
628     * @param queueCoordinator {@link QueueCoordinator} to use to set the blocks. Null if one should be created and enqueued
629     * @return {@link RoadRegenerateResult} result
630     * @since 6.6.0
631     */
632    public RoadRegenerateResult regenerateRoad(
633            final PlotArea area,
634            final BlockVector2 chunk,
635            int extend,
636            @Nullable QueueCoordinator queueCoordinator
637    ) {
638        int x = chunk.getX() << 4;
639        int z = chunk.getZ() << 4;
640        int ex = x + 15;
641        int ez = z + 15;
642        HybridPlotWorld plotWorld = (HybridPlotWorld) area;
643        if (!plotWorld.ROAD_SCHEMATIC_ENABLED) {
644            return RoadRegenerateResult.FAIL_ROAD_SCHEMATICS_NOT_ENABLED;
645        }
646        AtomicBoolean toCheck = new AtomicBoolean(false);
647        if (plotWorld.getType() == PlotAreaType.PARTIAL) {
648            boolean chunk1 = area.contains(x, z);
649            boolean chunk2 = area.contains(ex, ez);
650            if (!chunk1 && !chunk2) {
651                return RoadRegenerateResult.SUCCESS;
652            } else {
653                toCheck.set(chunk1 ^ chunk2);
654            }
655        }
656        PlotManager manager = area.getPlotManager();
657        PlotId id1 = manager.getPlotId(x, 0, z);
658        PlotId id2 = manager.getPlotId(ex, 0, ez);
659        x = x - plotWorld.ROAD_OFFSET_X;
660        z -= plotWorld.ROAD_OFFSET_Z;
661        int finalX = x;
662        int finalZ = z;
663        boolean enqueue;
664        QueueCoordinator queue;
665        if (queueCoordinator == null) {
666            queue = this.blockQueue.getNewQueue(worldUtil.getWeWorld(plotWorld.getWorldName()));
667            enqueue = true;
668        } else {
669            queue = queueCoordinator;
670            enqueue = false;
671        }
672        if (id1 == null || !id1.equals(id2)) {
673            if (id1 != null) {
674                Plot p1 = area.getPlotAbs(id1);
675                if (p1 != null && p1.hasOwner() && p1.isMerged()) {
676                    toCheck.set(true);
677                }
678            }
679            if (id2 != null && !toCheck.get()) {
680                Plot p2 = area.getPlotAbs(id2);
681                if (p2 != null && p2.hasOwner() && p2.isMerged()) {
682                    toCheck.set(true);
683                }
684            }
685            short size = plotWorld.SIZE;
686            for (int X = 0; X < 16; X++) {
687                short absX = (short) ((finalX + X) % size);
688                for (int Z = 0; Z < 16; Z++) {
689                    short absZ = (short) ((finalZ + Z) % size);
690                    if (absX < 0) {
691                        absX += size;
692                    }
693                    if (absZ < 0) {
694                        absZ += size;
695                    }
696                    boolean condition;
697                    if (toCheck.get()) {
698                        condition = manager.getPlotId(
699                                finalX + X + plotWorld.ROAD_OFFSET_X,
700                                1,
701                                finalZ + Z + plotWorld.ROAD_OFFSET_Z
702                        ) == null;
703                    } else {
704                        boolean gx = absX > plotWorld.PATH_WIDTH_LOWER;
705                        boolean gz = absZ > plotWorld.PATH_WIDTH_LOWER;
706                        boolean lx = absX < plotWorld.PATH_WIDTH_UPPER;
707                        boolean lz = absZ < plotWorld.PATH_WIDTH_UPPER;
708                        condition = !gx || !gz || !lx || !lz;
709                    }
710                    if (condition) {
711                        BaseBlock[] blocks = plotWorld.G_SCH.get(MathMan.pair(absX, absZ));
712                        int minY = plotWorld.getRoadYStart();
713                        int maxDy = Math.max(extend, blocks.length);
714                        for (int dy = 0; dy < maxDy; dy++) {
715                            if (dy > blocks.length - 1) {
716                                queue.setBlock(
717                                        finalX + X + plotWorld.ROAD_OFFSET_X,
718                                        minY + dy,
719                                        finalZ + Z + plotWorld.ROAD_OFFSET_Z,
720                                        WEExtent.AIRBASE
721                                );
722                            } else {
723                                BaseBlock block = blocks[dy];
724                                if (block != null) {
725                                    queue.setBlock(
726                                            finalX + X + plotWorld.ROAD_OFFSET_X,
727                                            minY + dy,
728                                            finalZ + Z + plotWorld.ROAD_OFFSET_Z,
729                                            block
730                                    );
731                                } else {
732                                    queue.setBlock(
733                                            finalX + X + plotWorld.ROAD_OFFSET_X,
734                                            minY + dy,
735                                            finalZ + Z + plotWorld.ROAD_OFFSET_Z,
736                                            WEExtent.AIRBASE
737                                    );
738                                }
739                            }
740                        }
741                        BiomeType biome = plotWorld.G_SCH_B.get(MathMan.pair(absX, absZ));
742                        if (biome != null) {
743                            queue.setBiome(finalX + X + plotWorld.ROAD_OFFSET_X, finalZ + Z + plotWorld.ROAD_OFFSET_Z, biome);
744                        } else {
745                            queue.setBiome(
746                                    finalX + X + plotWorld.ROAD_OFFSET_X,
747                                    finalZ + Z + plotWorld.ROAD_OFFSET_Z,
748                                    plotWorld.getPlotBiome()
749                            );
750                        }
751                    }
752                }
753            }
754            if (enqueue) {
755                queue.enqueue();
756            }
757        }
758        return RoadRegenerateResult.SUCCESS;
759    }
760
761    public enum RoadRegenerateResult {
762
763        SUCCESS(true, null),
764        FAIL_ROAD_SCHEMATICS_NOT_ENABLED(false, "Plot world does not have road schematics enabled - are they correctly set up?");
765
766        private final boolean recoverable;
767        private final String message;
768
769        RoadRegenerateResult(boolean recoverable, String message) {
770            this.recoverable = recoverable;
771            this.message = message;
772        }
773
774        public boolean irrecoverable() {
775            return !recoverable;
776        }
777
778        public String getMessage() {
779            return message;
780        }
781
782    }
783
784}