001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.bukkit.generator;
020
021import com.plotsquared.bukkit.queue.GenChunk;
022import com.plotsquared.bukkit.util.BukkitUtil;
023import com.plotsquared.bukkit.util.BukkitWorld;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.generator.ClassicPlotWorld;
026import com.plotsquared.core.generator.GeneratorWrapper;
027import com.plotsquared.core.generator.IndependentPlotGenerator;
028import com.plotsquared.core.generator.SingleWorldGenerator;
029import com.plotsquared.core.location.ChunkWrapper;
030import com.plotsquared.core.location.UncheckedWorldLocation;
031import com.plotsquared.core.plot.PlotArea;
032import com.plotsquared.core.plot.world.PlotAreaManager;
033import com.plotsquared.core.queue.ZeroedDelegateScopedQueueCoordinator;
034import com.plotsquared.core.util.ChunkManager;
035import com.sk89q.worldedit.bukkit.BukkitAdapter;
036import com.sk89q.worldedit.math.BlockVector2;
037import com.sk89q.worldedit.math.BlockVector3;
038import org.apache.logging.log4j.LogManager;
039import org.apache.logging.log4j.Logger;
040import org.bukkit.HeightMap;
041import org.bukkit.NamespacedKey;
042import org.bukkit.Registry;
043import org.bukkit.World;
044import org.bukkit.block.Biome;
045import org.bukkit.generator.BiomeProvider;
046import org.bukkit.generator.BlockPopulator;
047import org.bukkit.generator.ChunkGenerator;
048import org.bukkit.generator.WorldInfo;
049import org.checkerframework.checker.nullness.qual.NonNull;
050import org.jetbrains.annotations.NotNull;
051import org.jetbrains.annotations.Nullable;
052
053import java.util.ArrayList;
054import java.util.Arrays;
055import java.util.EnumSet;
056import java.util.List;
057import java.util.Random;
058import java.util.Set;
059
060import static java.util.function.Predicate.not;
061
062public class BukkitPlotGenerator extends ChunkGenerator implements GeneratorWrapper<ChunkGenerator> {
063
064    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + BukkitPlotGenerator.class.getSimpleName());
065
066    @SuppressWarnings("unused")
067    public final boolean PAPER_ASYNC_SAFE = true;
068
069    private final PlotAreaManager plotAreaManager;
070    private final IndependentPlotGenerator plotGenerator;
071    private final ChunkGenerator platformGenerator;
072    private final boolean full;
073    private final String levelName;
074    private final boolean useNewGenerationMethods;
075    private final BiomeProvider biomeProvider;
076    private List<BlockPopulator> populators;
077    private boolean loaded = false;
078
079    private PlotArea lastPlotArea;
080    private int lastChunkX = Integer.MIN_VALUE;
081    private int lastChunkZ = Integer.MIN_VALUE;
082
083    public BukkitPlotGenerator(
084            final @NonNull String name,
085            final @NonNull IndependentPlotGenerator generator,
086            final @NonNull PlotAreaManager plotAreaManager
087    ) {
088        this.plotAreaManager = plotAreaManager;
089        this.levelName = name;
090        this.plotGenerator = generator;
091        this.platformGenerator = this;
092        this.populators = new ArrayList<>();
093        int minecraftMinorVersion = PlotSquared.platform().serverVersion()[1];
094        if (minecraftMinorVersion >= 17) {
095            this.populators.add(new BlockStatePopulator(this.plotGenerator));
096        } else {
097            this.populators.add(new LegacyBlockStatePopulator(this.plotGenerator));
098        }
099        this.full = true;
100        this.useNewGenerationMethods = PlotSquared.platform().serverVersion()[1] >= 19;
101        this.biomeProvider = new BukkitPlotBiomeProvider();
102    }
103
104    public BukkitPlotGenerator(final String world, final ChunkGenerator cg, final @NonNull PlotAreaManager plotAreaManager) {
105        if (cg instanceof BukkitPlotGenerator) {
106            throw new IllegalArgumentException("ChunkGenerator: " + cg
107                    .getClass()
108                    .getName() + " is already a BukkitPlotGenerator!");
109        }
110        this.plotAreaManager = plotAreaManager;
111        this.levelName = world;
112        this.full = false;
113        this.platformGenerator = cg;
114        this.plotGenerator = new DelegatePlotGenerator(cg, world);
115        this.useNewGenerationMethods = PlotSquared.platform().serverVersion()[1] >= 19;
116        this.biomeProvider = null;
117    }
118
119    @Override
120    public void augment(PlotArea area) {
121        BukkitAugmentedGenerator.get(BukkitUtil.getWorld(area.getWorldName()));
122    }
123
124    @Override
125    public boolean isFull() {
126        return this.full;
127    }
128
129    @Override
130    public IndependentPlotGenerator getPlotGenerator() {
131        return this.plotGenerator;
132    }
133
134    @Override
135    public ChunkGenerator getPlatformGenerator() {
136        return this.platformGenerator;
137    }
138
139    @Override
140    public @NonNull List<BlockPopulator> getDefaultPopulators(@NonNull World world) {
141        try {
142            checkLoaded(world);
143        } catch (Exception e) {
144            LOGGER.error("Error attempting to load world into PlotSquared.", e);
145        }
146        ArrayList<BlockPopulator> toAdd = new ArrayList<>();
147        List<BlockPopulator> existing = world.getPopulators();
148        if (populators == null && platformGenerator != null) {
149            populators = new ArrayList<>(platformGenerator.getDefaultPopulators(world));
150        }
151        if (populators != null) {
152            for (BlockPopulator populator : this.populators) {
153                if (!existing.contains(populator)) {
154                    toAdd.add(populator);
155                }
156            }
157        }
158        return toAdd;
159    }
160
161    // Extracted to synchronized method for thread-safety, preventing multiple internal world load calls
162    private synchronized void checkLoaded(@NonNull World world) {
163        // Do not attempt to load configurations until WorldEdit has a platform ready.
164        if (!PlotSquared.get().isWeInitialised()) {
165            return;
166        }
167        if (!this.loaded) {
168            String name = world.getName();
169            PlotSquared.get().loadWorld(name, this);
170            final Set<PlotArea> areas = this.plotAreaManager.getPlotAreasSet(name);
171            if (!areas.isEmpty()) {
172                PlotArea area = areas.iterator().next();
173                if (!area.isMobSpawning()) {
174                    if (!area.isSpawnEggs()) {
175                        world.setSpawnFlags(false, false);
176                    }
177                    setSpawnLimits(world, 0);
178                } else {
179                    world.setSpawnFlags(true, true);
180                    setSpawnLimits(world, -1);
181                }
182            }
183            this.loaded = true;
184        }
185    }
186
187    @SuppressWarnings("deprecation") // Kept for compatibility with <=1.17.1
188    private void setSpawnLimits(@NonNull World world, int limit) {
189        world.setAmbientSpawnLimit(limit);
190        world.setAnimalSpawnLimit(limit);
191        world.setMonsterSpawnLimit(limit);
192        world.setWaterAnimalSpawnLimit(limit);
193    }
194
195    @Override
196    public void generateNoise(
197            @NotNull final WorldInfo worldInfo,
198            @NotNull final Random random,
199            final int chunkX,
200            final int chunkZ,
201            @NotNull final ChunkData chunkData
202    ) {
203        if (this.platformGenerator != this) {
204            this.platformGenerator.generateNoise(worldInfo, random, chunkX, chunkZ, chunkData);
205            return;
206        }
207        int minY = chunkData.getMinHeight();
208        int maxY = chunkData.getMaxHeight();
209        GenChunk result = new GenChunk(minY, maxY);
210        // Set the chunk location
211        result.setChunk(new ChunkWrapper(worldInfo.getName(), chunkX, chunkZ));
212        // Set the result data
213        result.setChunkData(chunkData);
214        result.result = null;
215
216        // Catch any exceptions (as exceptions usually thrown)
217        try {
218            generate(BlockVector2.at(chunkX, chunkZ), worldInfo.getName(), result, false);
219        } catch (Throwable e) {
220            LOGGER.error("Error attempting to generate chunk.", e);
221        }
222    }
223
224    @Override
225    public void generateSurface(
226            @NotNull final WorldInfo worldInfo,
227            @NotNull final Random random,
228            final int chunkX,
229            final int chunkZ,
230            @NotNull final ChunkData chunkData
231    ) {
232        if (platformGenerator != this) {
233            platformGenerator.generateSurface(worldInfo, random, chunkX, chunkZ, chunkData);
234        }
235    }
236
237    @Override
238    public void generateBedrock(
239            @NotNull final WorldInfo worldInfo,
240            @NotNull final Random random,
241            final int chunkX,
242            final int chunkZ,
243            @NotNull final ChunkData chunkData
244    ) {
245        if (platformGenerator != this) {
246            platformGenerator.generateBedrock(worldInfo, random, chunkX, chunkZ, chunkData);
247        }
248    }
249
250    @Override
251    public void generateCaves(
252            @NotNull final WorldInfo worldInfo,
253            @NotNull final Random random,
254            final int chunkX,
255            final int chunkZ,
256            @NotNull final ChunkData chunkData
257    ) {
258        if (platformGenerator != this) {
259            platformGenerator.generateCaves(worldInfo, random, chunkX, chunkZ, chunkData);
260        }
261    }
262
263    @Override
264    public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull final WorldInfo worldInfo) {
265        if (platformGenerator != this) {
266            return platformGenerator.getDefaultBiomeProvider(worldInfo);
267        }
268        return biomeProvider;
269    }
270
271    @Override
272    public int getBaseHeight(
273            @NotNull final WorldInfo worldInfo,
274            @NotNull final Random random,
275            final int x,
276            final int z,
277            @NotNull final HeightMap heightMap
278    ) {
279        PlotArea area = getPlotArea(worldInfo.getName(), x, z);
280        if (area instanceof ClassicPlotWorld cpw) {
281            // Default to plot height being the heighest point before decoration (i.e. roads, walls etc.)
282            return cpw.PLOT_HEIGHT;
283        }
284        return super.getBaseHeight(worldInfo, random, x, z, heightMap);
285    }
286
287    /**
288     * The entire method is deprecated, but kept for compatibility with versions lower than or equal to 1.16.2.
289     * The method will be removed in future versions, because WorldEdit and FastAsyncWorldEdit only support the latest point
290     * release.
291     */
292    @SuppressWarnings("deprecation") // The entire method is deprecated, but kept for compatibility with <=1.16.2
293    @Override
294    @Deprecated(since = "7.0.0")
295    public @NonNull ChunkData generateChunkData(
296            @NonNull World world, @NonNull Random random, int x, int z, @NonNull BiomeGrid biome
297    ) {
298        if (useNewGenerationMethods) {
299            if (this.platformGenerator != this) {
300                return this.platformGenerator.generateChunkData(world, random, x, z, biome);
301            } else {
302                // Throw exception to be caught by the server that indicates the new generation API is being used.
303                throw new UnsupportedOperationException("Using new generation methods. This method is unsupported.");
304            }
305        }
306
307        int minY = BukkitWorld.getMinWorldHeight(world);
308        int maxY = BukkitWorld.getMaxWorldHeight(world);
309        GenChunk result = new GenChunk(minY, maxY);
310        if (this.getPlotGenerator() instanceof SingleWorldGenerator) {
311            if (result.getChunkData() != null) {
312                for (int chunkX = 0; chunkX < 16; chunkX++) {
313                    for (int chunkZ = 0; chunkZ < 16; chunkZ++) {
314                        for (int y = minY; y < maxY; y++) {
315                            biome.setBiome(chunkX, y, chunkZ, Biome.PLAINS);
316                        }
317                    }
318                }
319                return result.getChunkData();
320            }
321        }
322        // Set the chunk location
323        result.setChunk(new ChunkWrapper(world.getName(), x, z));
324        // Set the result data
325        result.setChunkData(createChunkData(world));
326        result.biomeGrid = biome;
327        result.result = null;
328
329        // Catch any exceptions (as exceptions usually thrown)
330        try {
331            // Fill the result data if necessary
332            if (this.platformGenerator != this) {
333                return this.platformGenerator.generateChunkData(world, random, x, z, biome);
334            } else {
335                generate(BlockVector2.at(x, z), world.getName(), result, true);
336            }
337        } catch (Throwable e) {
338            LOGGER.error("Error attempting to load world into PlotSquared.", e);
339        }
340        // Return the result data
341        return result.getChunkData();
342    }
343
344    private void generate(BlockVector2 loc, String world, ZeroedDelegateScopedQueueCoordinator result, boolean biomes) {
345        // Load if improperly loaded
346        if (!this.loaded) {
347            synchronized (this) {
348                PlotSquared.get().loadWorld(world, this);
349            }
350        }
351        // Process the chunk
352        if (ChunkManager.preProcessChunk(loc, result)) {
353            return;
354        }
355        PlotArea area = getPlotArea(world, loc.getX(), loc.getZ());
356        try {
357            this.plotGenerator.generateChunk(result, area, biomes);
358        } catch (Throwable e) {
359            // Recover from generator error
360            LOGGER.error("Error attempting to generate chunk.", e);
361        }
362        ChunkManager.postProcessChunk(loc, result);
363    }
364
365    @Override
366    public boolean canSpawn(final @NonNull World world, final int x, final int z) {
367        return true;
368    }
369
370    public boolean shouldGenerateCaves() {
371        return false;
372    }
373
374    public boolean shouldGenerateDecorations() {
375        return false;
376    }
377
378    public boolean isParallelCapable() {
379        return true;
380    }
381
382    public boolean shouldGenerateMobs() {
383        return false;
384    }
385
386    public boolean shouldGenerateStructures() {
387        return true;
388    }
389
390    @Override
391    public String toString() {
392        if (this.platformGenerator == this) {
393            return this.plotGenerator.getName();
394        }
395        if (this.platformGenerator == null) {
396            return "null";
397        } else {
398            return this.platformGenerator.getClass().getName();
399        }
400    }
401
402    @Override
403    public boolean equals(final Object obj) {
404        if (obj == null) {
405            return false;
406        }
407        return toString().equals(obj.toString()) || toString().equals(obj.getClass().getName());
408    }
409
410    public String getLevelName() {
411        return this.levelName;
412    }
413
414    private synchronized PlotArea getPlotArea(String name, int chunkX, int chunkZ) {
415        // Load if improperly loaded
416        if (!this.loaded) {
417            PlotSquared.get().loadWorld(name, this);
418            // Do not set loaded to true as we want to ensure spawn limits are set when "loading" is actually able to be
419            // completed properly.
420        }
421        if (lastPlotArea != null && name.equals(this.levelName) && chunkX == lastChunkX && chunkZ == lastChunkZ) {
422            return lastPlotArea;
423        }
424        BlockVector3 loc = BlockVector3.at(chunkX << 4, 0, chunkZ << 4);
425        if (lastPlotArea != null && lastPlotArea.getRegion().contains(loc) && lastPlotArea.getRegion().contains(loc)) {
426            return lastPlotArea;
427        }
428        PlotArea area = UncheckedWorldLocation.at(name, loc).getPlotArea();
429        if (area == null) {
430            throw new IllegalStateException(String.format(
431                    "Cannot generate chunk that does not belong to a plot area. World: %s",
432                    name
433            ));
434        }
435        this.lastChunkX = chunkX;
436        this.lastChunkZ = chunkZ;
437        return this.lastPlotArea = area;
438    }
439
440    /**
441     * Biome provider should never need to be accessed outside of this class.
442     */
443    private final class BukkitPlotBiomeProvider extends BiomeProvider {
444
445        private static final List<Biome> BIOMES;
446
447        static {
448            Set<Biome> disabledBiomes = EnumSet.of(Biome.CUSTOM);
449            if (PlotSquared.platform().serverVersion()[1] <= 19) {
450                final Biome cherryGrove = Registry.BIOME.get(NamespacedKey.minecraft("cherry_grove"));
451                if (cherryGrove != null) {
452                    disabledBiomes.add(cherryGrove);
453                }
454            }
455            BIOMES = Arrays.stream(Biome.values())
456                    .filter(not(disabledBiomes::contains))
457                    .toList();
458        }
459
460        @Override
461        public @NotNull Biome getBiome(@NotNull final WorldInfo worldInfo, final int x, final int y, final int z) {
462            PlotArea area = getPlotArea(worldInfo.getName(), x >> 4, z >> 4);
463            return BukkitAdapter.adapt(plotGenerator.getBiome(area, x, y, z));
464        }
465
466        @Override
467        public @NotNull List<Biome> getBiomes(@NotNull final WorldInfo worldInfo) {
468            return BIOMES; // Allow all biomes
469        }
470
471    }
472
473}