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