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.google.inject.assistedinject.Assisted;
023import com.intellectualsites.annotations.DoNotUse;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.configuration.ConfigurationSection;
026import com.plotsquared.core.configuration.Settings;
027import com.plotsquared.core.configuration.file.YamlConfiguration;
028import com.plotsquared.core.inject.annotations.WorldConfig;
029import com.plotsquared.core.inject.factory.ProgressSubscriberFactory;
030import com.plotsquared.core.location.Location;
031import com.plotsquared.core.plot.Plot;
032import com.plotsquared.core.plot.PlotArea;
033import com.plotsquared.core.plot.PlotId;
034import com.plotsquared.core.plot.PlotManager;
035import com.plotsquared.core.plot.schematic.Schematic;
036import com.plotsquared.core.queue.GlobalBlockQueue;
037import com.plotsquared.core.util.FileUtils;
038import com.plotsquared.core.util.MathMan;
039import com.plotsquared.core.util.SchematicHandler;
040import com.sk89q.jnbt.CompoundTag;
041import com.sk89q.jnbt.CompoundTagBuilder;
042import com.sk89q.worldedit.entity.Entity;
043import com.sk89q.worldedit.extent.clipboard.Clipboard;
044import com.sk89q.worldedit.extent.transform.BlockTransformExtent;
045import com.sk89q.worldedit.internal.helper.MCDirections;
046import com.sk89q.worldedit.math.BlockVector2;
047import com.sk89q.worldedit.math.BlockVector3;
048import com.sk89q.worldedit.math.Vector3;
049import com.sk89q.worldedit.math.transform.AffineTransform;
050import com.sk89q.worldedit.util.Direction;
051import com.sk89q.worldedit.world.biome.BiomeType;
052import com.sk89q.worldedit.world.block.BaseBlock;
053import org.apache.logging.log4j.LogManager;
054import org.apache.logging.log4j.Logger;
055import org.checkerframework.checker.nullness.qual.NonNull;
056import org.checkerframework.checker.nullness.qual.Nullable;
057
058import java.io.File;
059import java.lang.reflect.Field;
060import java.util.ArrayList;
061import java.util.HashMap;
062import java.util.List;
063import java.util.Locale;
064
065public class HybridPlotWorld extends ClassicPlotWorld {
066
067    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + HybridPlotWorld.class.getSimpleName());
068    private static final AffineTransform transform = new AffineTransform().rotateY(90);
069    public boolean ROAD_SCHEMATIC_ENABLED;
070    public boolean PLOT_SCHEMATIC = false;
071    @Deprecated(forRemoval = true, since = "6.9.0")
072    public int PLOT_SCHEMATIC_HEIGHT = -1;
073    public short PATH_WIDTH_LOWER;
074    public short PATH_WIDTH_UPPER;
075    public HashMap<Integer, BaseBlock[]> G_SCH;
076    public HashMap<Integer, BiomeType> G_SCH_B;
077    /**
078     * The Y level at which schematic generation will start, lowest of either road or plot schematic generation.
079     */
080    public int SCHEM_Y;
081    private Location SIGN_LOCATION;
082    private File root = null;
083    private int lastOverlayHeightError = Integer.MIN_VALUE;
084    private List<Entity> schem3Entities = null;
085    private BlockVector3 schem3MinPoint = null;
086    private boolean schem1PopulationNeeded = false;
087    private boolean schem2PopulationNeeded = false;
088    private boolean schem3PopulationNeeded = false;
089
090    @Inject
091    private SchematicHandler schematicHandler;
092
093    @Inject
094    public HybridPlotWorld(
095            @Assisted("world") final String worldName,
096            @javax.annotation.Nullable @Assisted("id") final String id,
097            @Assisted final @NonNull IndependentPlotGenerator generator,
098            @javax.annotation.Nullable @Assisted("min") final PlotId min,
099            @javax.annotation.Nullable @Assisted("max") final PlotId max,
100            @WorldConfig final @NonNull YamlConfiguration worldConfiguration,
101            final @NonNull GlobalBlockQueue blockQueue
102    ) {
103        super(worldName, id, generator, min, max, worldConfiguration, blockQueue);
104        PlotSquared.platform().injector().injectMembers(this);
105    }
106
107    @Deprecated(forRemoval = true, since = "6.9.0")
108    public static byte wrap(byte data, int start) {
109        if ((data >= start) && (data < (start + 4))) {
110            data = (byte) ((((data - start) + 2) & 3) + start);
111        }
112        return data;
113    }
114
115    @Deprecated(forRemoval = true, since = "6.9.0")
116    public static byte wrap2(byte data, int start) {
117        if ((data >= start) && (data < (start + 2))) {
118            data = (byte) ((((data - start) + 1) & 1) + start);
119        }
120        return data;
121    }
122
123    public static BaseBlock rotate(BaseBlock id) {
124
125        CompoundTag tag = id.getNbtData();
126
127        if (tag != null) {
128            // Handle blocks which store their rotation in NBT
129            if (tag.containsKey("Rot")) {
130                int rot = tag.asInt("Rot");
131
132                Direction direction = MCDirections.fromRotation(rot);
133
134                if (direction != null) {
135                    Vector3 vector = transform.apply(direction.toVector()).subtract(transform.apply(Vector3.ZERO)).normalize();
136                    Direction newDirection =
137                            Direction.findClosest(
138                                    vector,
139                                    Direction.Flag.CARDINAL | Direction.Flag.ORDINAL | Direction.Flag.SECONDARY_ORDINAL
140                            );
141
142                    if (newDirection != null) {
143                        CompoundTagBuilder builder = tag.createBuilder();
144
145                        builder.putByte("Rot", (byte) MCDirections.toRotation(newDirection));
146
147                        id.setNbtData(builder.build());
148                    }
149                }
150            }
151        }
152        return BlockTransformExtent.transform(id, transform);
153    }
154
155    @NonNull
156    @Override
157    protected PlotManager createManager() {
158        return new HybridPlotManager(this, PlotSquared.platform().regionManager(),
159                PlotSquared.platform().injector().getInstance(ProgressSubscriberFactory.class)
160        );
161    }
162
163    public Location getSignLocation(@NonNull Plot plot) {
164        plot = plot.getBasePlot(false);
165        final Location bot = plot.getBottomAbs();
166        if (SIGN_LOCATION == null) {
167            return bot.withY(ROAD_HEIGHT + 1).add(-1, 0, -2);
168        } else {
169            return bot.withY(0).add(SIGN_LOCATION.getX(), SIGN_LOCATION.getY(), SIGN_LOCATION.getZ());
170        }
171    }
172
173    /**
174     * <p>This method is called when a world loads. Make sure you set all your constants here. You are provided with the
175     * configuration section for that specific world.</p>
176     */
177    @Override
178    public void loadConfiguration(ConfigurationSection config) {
179        super.loadConfiguration(config);
180        if ((this.ROAD_WIDTH & 1) == 0) {
181            this.PATH_WIDTH_LOWER = (short) (Math.floor(this.ROAD_WIDTH / 2f) - 1);
182        } else {
183            this.PATH_WIDTH_LOWER = (short) Math.floor(this.ROAD_WIDTH / 2f);
184        }
185        if (this.ROAD_WIDTH == 0) {
186            this.PATH_WIDTH_UPPER = (short) (this.SIZE + 1);
187        } else {
188            this.PATH_WIDTH_UPPER = (short) (this.PATH_WIDTH_LOWER + this.PLOT_WIDTH + 1);
189        }
190        try {
191            setupSchematics();
192        } catch (Exception event) {
193            event.printStackTrace();
194        }
195
196        // Dump world settings
197        if (Settings.DEBUG) {
198            LOGGER.info("- Dumping settings for ClassicPlotWorld with name {}", this.getWorldName());
199            final Field[] fields = this.getClass().getFields();
200            for (final Field field : fields) {
201                final String name = field.getName().toLowerCase(Locale.ENGLISH);
202                if (name.contains("g_sch")) {
203                    continue;
204                }
205                Object value;
206                try {
207                    final boolean accessible = field.isAccessible();
208                    field.setAccessible(true);
209                    value = field.get(this);
210                    field.setAccessible(accessible);
211                } catch (final IllegalAccessException e) {
212                    value = String.format("Failed to parse: %s", e.getMessage());
213                }
214                LOGGER.info("-- {} = {}", name, value);
215            }
216        }
217    }
218
219    @Override
220    public boolean isCompatible(final @NonNull PlotArea plotArea) {
221        if (!(plotArea instanceof SquarePlotWorld)) {
222            return false;
223        }
224        return ((SquarePlotWorld) plotArea).PLOT_WIDTH == this.PLOT_WIDTH;
225    }
226
227    public void setupSchematics() throws SchematicHandler.UnsupportedFormatException {
228        this.G_SCH = new HashMap<>();
229        this.G_SCH_B = new HashMap<>();
230
231        // Try to determine root. This means that plot areas can have separate schematic
232        // directories
233        if (!(root =
234                FileUtils.getFile(
235                        PlotSquared.platform().getDirectory(),
236                        "schematics/GEN_ROAD_SCHEMATIC/" + this.getWorldName() + "/" + this.getId()
237                ))
238                .exists()) {
239            root = FileUtils.getFile(
240                    PlotSquared.platform().getDirectory(),
241                    "schematics/GEN_ROAD_SCHEMATIC/" + this.getWorldName()
242            );
243        }
244
245        File schematic1File = new File(root, "sideroad.schem");
246        if (!schematic1File.exists()) {
247            schematic1File = new File(root, "sideroad.schematic");
248        }
249        File schematic2File = new File(root, "intersection.schem");
250        if (!schematic2File.exists()) {
251            schematic2File = new File(root, "intersection.schematic");
252        }
253        File schematic3File = new File(root, "plot.schem");
254        if (!schematic3File.exists()) {
255            schematic3File = new File(root, "plot.schematic");
256        }
257        Schematic schematic1 = this.schematicHandler.getSchematic(schematic1File);
258        Schematic schematic2 = this.schematicHandler.getSchematic(schematic2File);
259        Schematic schematic3 = this.schematicHandler.getSchematic(schematic3File);
260
261        // If the plot schematic contains entities, then they need to be populated upon generation.
262        if (schematic3 != null && !schematic3.getClipboard().getEntities().isEmpty()) {
263            this.schem3Entities = new ArrayList<>(schematic3.getClipboard().getEntities());
264            this.schem3MinPoint = schematic3.getClipboard().getMinimumPoint();
265            this.schem3PopulationNeeded = true;
266        }
267
268        int shift = this.ROAD_WIDTH / 2;
269        int oddshift = (this.ROAD_WIDTH & 1);
270
271        SCHEM_Y = schematicStartHeight();
272
273        // plotY and roadY are important to allow plot and/or road schematic "overflow" into each other without causing AIOOB
274        //  exceptions when attempting either to set blocks to, or get block from G_SCH
275        // Default plot schematic start height, normalized to the minimum height schematics are pasted from.
276        int plotY = PLOT_HEIGHT - SCHEM_Y;
277        int minRoadWall = Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT ? Math.min(ROAD_HEIGHT, WALL_HEIGHT) : ROAD_HEIGHT;
278        // Default road schematic start height, normalized to the minimum height schematics are pasted from.
279        int roadY = minRoadWall - SCHEM_Y;
280
281        int worldGenHeight = getMaxGenHeight() - getMinGenHeight() + 1;
282
283        int maxSchematicHeight = 0;
284        int plotSchemHeight = 0;
285
286        // SCHEM_Y should be normalised to the plot "start" height
287        if (schematic3 != null) {
288            plotSchemHeight = maxSchematicHeight = schematic3.getClipboard().getDimensions().getY();
289            if (maxSchematicHeight == worldGenHeight) {
290                SCHEM_Y = getMinGenHeight();
291                plotY = 0;
292            } else if (!Settings.Schematics.PASTE_ON_TOP) {
293                SCHEM_Y = getMinBuildHeight();
294                plotY = 0;
295            }
296        }
297
298        int roadSchemHeight;
299
300        if (schematic1 != null) {
301            roadSchemHeight = Math.max(
302                    schematic1.getClipboard().getDimensions().getY(),
303                    schematic2.getClipboard().getDimensions().getY()
304            );
305            maxSchematicHeight = Math.max(roadSchemHeight, maxSchematicHeight);
306            if (maxSchematicHeight == worldGenHeight) {
307                SCHEM_Y = getMinGenHeight();
308                roadY = 0; // Road is the lowest schematic
309                if (schematic3 != null && schematic3.getClipboard().getDimensions().getY() != worldGenHeight) {
310                    // Road is the lowest schematic. Normalize plotY to it.
311                    if (Settings.Schematics.PASTE_ON_TOP) {
312                        plotY = PLOT_HEIGHT - getMinGenHeight();
313                    } else {
314                        plotY = getMinBuildHeight() - getMinGenHeight();
315                    }
316                }
317            } else if (!Settings.Schematics.PASTE_ROAD_ON_TOP) {
318                if (SCHEM_Y == getMinGenHeight()) { // Only possible if plot schematic is enabled
319                    // Plot is still the lowest schematic, normalize roadY to it
320                    roadY = getMinBuildHeight() - getMinGenHeight();
321                } else if (schematic3 != null) {
322                    SCHEM_Y = getMinBuildHeight();
323                    roadY = 0;// Road is the lowest schematic
324                    if (Settings.Schematics.PASTE_ON_TOP) {
325                        // Road is the lowest schematic. Normalize plotY to it.
326                        plotY = PLOT_HEIGHT - getMinBuildHeight();
327                    }
328                    maxSchematicHeight = Math.max(maxSchematicHeight, plotY + plotSchemHeight);
329                }
330            } else {
331                roadY = minRoadWall - SCHEM_Y;
332                maxSchematicHeight = Math.max(maxSchematicHeight, roadY + roadSchemHeight);
333            }
334        }
335
336        if (schematic3 != null) {
337            this.PLOT_SCHEMATIC = true;
338            Clipboard blockArrayClipboard3 = schematic3.getClipboard();
339
340            BlockVector3 d3 = blockArrayClipboard3.getDimensions();
341            short w3 = (short) d3.getX();
342            short l3 = (short) d3.getZ();
343            short h3 = (short) d3.getY();
344            if (w3 > PLOT_WIDTH || h3 > PLOT_WIDTH) {
345                this.ROAD_SCHEMATIC_ENABLED = true;
346            }
347            int centerShiftZ;
348            if (l3 < this.PLOT_WIDTH) {
349                centerShiftZ = (this.PLOT_WIDTH - l3) / 2;
350            } else {
351                centerShiftZ = (PLOT_WIDTH - l3) / 2;
352            }
353            int centerShiftX;
354            if (w3 < this.PLOT_WIDTH) {
355                centerShiftX = (this.PLOT_WIDTH - w3) / 2;
356            } else {
357                centerShiftX = (PLOT_WIDTH - w3) / 2;
358            }
359
360            BlockVector3 min = blockArrayClipboard3.getMinimumPoint();
361            for (short x = 0; x < w3; x++) {
362                for (short z = 0; z < l3; z++) {
363                    for (short y = 0; y < h3; y++) {
364                        BaseBlock id = blockArrayClipboard3.getFullBlock(BlockVector3.at(
365                                x + min.getBlockX(),
366                                y + min.getBlockY(),
367                                z + min.getBlockZ()
368                        ));
369                        schem3PopulationNeeded |= id.hasNbtData();
370                        addOverlayBlock(
371                                (short) (x + shift + oddshift + centerShiftX),
372                                (short) (y + plotY),
373                                (short) (z + shift + oddshift + centerShiftZ),
374                                id,
375                                false,
376                                maxSchematicHeight
377                        );
378                    }
379                    if (blockArrayClipboard3.hasBiomes()) {
380                        BiomeType biome = blockArrayClipboard3.getBiome(BlockVector2.at(
381                                x + min.getBlockX(),
382                                z + min.getBlockZ()
383                        ));
384                        addOverlayBiome(
385                                (short) (x + shift + oddshift + centerShiftX),
386                                (short) (z + shift + oddshift + centerShiftZ),
387                                biome
388                        );
389                    }
390                }
391            }
392
393            if (Settings.DEBUG) {
394                LOGGER.info("- plot schematic: {}", schematic3File.getPath());
395            }
396        }
397        if ((schematic1 == null && schematic2 == null) || this.ROAD_WIDTH == 0) {
398            if (Settings.DEBUG) {
399                LOGGER.info("- schematic: false");
400            }
401            return;
402        }
403        this.ROAD_SCHEMATIC_ENABLED = true;
404        // Do not populate road if using schematic population
405        // TODO: What? this.ROAD_BLOCK = BlockBucket.empty(); // BlockState.getEmptyData(this.ROAD_BLOCK); // BlockUtil.get(this.ROAD_BLOCK.id, (byte) 0);
406
407        Clipboard blockArrayClipboard1 = schematic1.getClipboard();
408
409        BlockVector3 d1 = blockArrayClipboard1.getDimensions();
410        short w1 = (short) d1.getX();
411        short l1 = (short) d1.getZ();
412        short h1 = (short) d1.getY();
413        // Workaround for schematic height issue if proper calculation of road schematic height is disabled
414        if (!Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT) {
415            h1 += Math.max(ROAD_HEIGHT - WALL_HEIGHT, 0);
416        }
417
418        BlockVector3 min = blockArrayClipboard1.getMinimumPoint();
419        for (short x = 0; x < w1; x++) {
420            for (short z = 0; z < l1; z++) {
421                for (short y = 0; y < h1; y++) {
422                    BaseBlock id = blockArrayClipboard1.getFullBlock(BlockVector3.at(
423                            x + min.getBlockX(),
424                            y + min.getBlockY(),
425                            z + min.getBlockZ()
426                    ));
427                    schem1PopulationNeeded |= id.hasNbtData();
428                    addOverlayBlock(
429                            (short) (x - shift),
430                            (short) (y + roadY),
431                            (short) (z + shift + oddshift),
432                            id,
433                            false,
434                            maxSchematicHeight
435                    );
436                    addOverlayBlock(
437                            (short) (z + shift + oddshift),
438                            (short) (y + roadY),
439                            (short) (shift - x + (oddshift - 1)),
440                            id,
441                            true,
442                            maxSchematicHeight
443                    );
444                }
445                if (blockArrayClipboard1.hasBiomes()) {
446                    BiomeType biome = blockArrayClipboard1.getBiome(BlockVector2.at(x + min.getBlockX(), z + min.getBlockZ()));
447                    addOverlayBiome((short) (x - shift), (short) (z + shift + oddshift), biome);
448                    addOverlayBiome((short) (z + shift + oddshift), (short) (shift - x + (oddshift - 1)), biome);
449                }
450            }
451        }
452
453        Clipboard blockArrayClipboard2 = schematic2.getClipboard();
454        BlockVector3 d2 = blockArrayClipboard2.getDimensions();
455        short w2 = (short) d2.getX();
456        short l2 = (short) d2.getZ();
457        short h2 = (short) d2.getY();
458        // Workaround for schematic height issue if proper calculation of road schematic height is disabled
459        if (!Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT) {
460            h2 += Math.max(ROAD_HEIGHT - WALL_HEIGHT, 0);
461        }
462        min = blockArrayClipboard2.getMinimumPoint();
463        for (short x = 0; x < w2; x++) {
464            for (short z = 0; z < l2; z++) {
465                for (short y = 0; y < h2; y++) {
466                    BaseBlock id = blockArrayClipboard2.getFullBlock(BlockVector3.at(
467                            x + min.getBlockX(),
468                            y + min.getBlockY(),
469                            z + min.getBlockZ()
470                    ));
471                    schem2PopulationNeeded |= id.hasNbtData();
472                    addOverlayBlock(
473                            (short) (x - shift),
474                            (short) (y + roadY),
475                            (short) (z - shift),
476                            id,
477                            false,
478                            maxSchematicHeight
479                    );
480                }
481                if (blockArrayClipboard2.hasBiomes()) {
482                    BiomeType biome = blockArrayClipboard2.getBiome(BlockVector2.at(x + min.getBlockX(), z + min.getBlockZ()));
483                    addOverlayBiome((short) (x - shift), (short) (z - shift), biome);
484                }
485            }
486        }
487    }
488
489    /**
490     * @deprecated This method should not be available for public API usage and will be made private.
491     */
492    @Deprecated(forRemoval = true, since = "6.10.2")
493    public void addOverlayBlock(short x, short y, short z, BaseBlock id, boolean rotate, int height) {
494        if (z < 0) {
495            z += this.SIZE;
496        } else if (z >= this.SIZE) {
497            z -= this.SIZE;
498        }
499        if (x < 0) {
500            x += this.SIZE;
501        } else if (x >= this.SIZE) {
502            x -= this.SIZE;
503        }
504        if (rotate) {
505            id = rotate(id);
506        }
507        int pair = MathMan.pair(x, z);
508        BaseBlock[] existing = this.G_SCH.computeIfAbsent(pair, k -> new BaseBlock[height]);
509        if (y >= height) {
510            if (y > lastOverlayHeightError) {
511                lastOverlayHeightError = y;
512                LOGGER.error(
513                        "Error adding overlay block in world {}. `y > height`. y={}, height={}",
514                        getWorldName(),
515                        y,
516                        height
517                );
518            }
519            return;
520        }
521        existing[y] = id;
522    }
523
524    /**
525     * @deprecated This method should not be available for public API usage and will be made private.
526     */
527    @Deprecated(forRemoval = true, since = "6.10.2")
528    public void addOverlayBiome(short x, short z, BiomeType id) {
529        if (z < 0) {
530            z += this.SIZE;
531        } else if (z >= this.SIZE) {
532            z -= this.SIZE;
533        }
534        if (x < 0) {
535            x += this.SIZE;
536        } else if (x >= this.SIZE) {
537            x -= this.SIZE;
538        }
539        int pair = MathMan.pair(x, z);
540        this.G_SCH_B.put(pair, id);
541    }
542
543    /**
544     * Get the entities contained within the plot schematic for generation. Intended for internal use only.
545     *
546     * @since 6.9.0
547     */
548    @DoNotUse
549    public @Nullable List<Entity> getPlotSchematicEntities() {
550        return schem3Entities;
551    }
552
553    /**
554     * Get the minimum point of the plot schematic for generation. Intended for internal use only.
555     *
556     * @since 6.9.0
557     */
558    @DoNotUse
559    public @Nullable BlockVector3 getPlotSchematicMinPoint() {
560        return schem3MinPoint;
561    }
562
563    /**
564     * Get if post-generation population of chunks with tiles/entities is needed for this world. Not for public API use.
565     *
566     * @since 6.9.0
567     */
568    @DoNotUse
569    public boolean populationNeeded() {
570        return schem1PopulationNeeded || schem2PopulationNeeded || schem3PopulationNeeded;
571    }
572
573    /**
574     * @deprecated in favour of {@link HybridPlotWorld#getSchematicRoot()}
575     */
576    @Deprecated(forRemoval = true, since = "6.9.0")
577    public File getRoot() {
578        return this.root;
579    }
580
581    /**
582     * Get the root folder for this world's generation schematics. May be null if schematics not initialised via
583     * {@link HybridPlotWorld#setupSchematics()}
584     *
585     * @since 6.9.0
586     */
587    public @Nullable File getSchematicRoot() {
588        return this.root;
589    }
590
591}