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