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}