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 // Schematics should generate/be pasted from build height 280 SCHEM_Y = getMinBuildHeight(); 281 plotY = 0; 282 } 283 } 284 285 int roadSchemHeight = 0; 286 287 if (schematic1 != null) { 288 roadSchemHeight = Math.max( 289 schematic1.getClipboard().getDimensions().getY(), 290 schematic2.getClipboard().getDimensions().getY() 291 ); 292 if (roadSchemHeight == worldGenHeight) { 293 SCHEM_Y = getMinGenHeight(); 294 roadY = 0; // Road is the lowest schematic 295 if (schematic3 != null && schematic3.getClipboard().getDimensions().getY() != worldGenHeight) { 296 // Road is the lowest schematic. Normalize plotY to it. 297 plotY = (Settings.Schematics.PASTE_ON_TOP ? PLOT_HEIGHT : getMinBuildHeight()) - SCHEM_Y; 298 } 299 } else if (!Settings.Schematics.PASTE_ROAD_ON_TOP) { 300 roadY = 0; 301 SCHEM_Y = getMinGenHeight(); 302 if (schematic3 != null && schematic3.getClipboard().getDimensions().getY() != worldGenHeight) { 303 // Road is the lowest schematic. Normalize plotY to it. 304 plotY = (Settings.Schematics.PASTE_ON_TOP ? PLOT_HEIGHT : getMinBuildHeight()) - SCHEM_Y; 305 } 306 } else { 307 roadY = minRoadWall - SCHEM_Y; 308 } 309 } 310 int maxSchematicHeight = Math.max(plotY + plotSchemHeight, roadY + roadSchemHeight); 311 312 if (schematic3 != null) { 313 this.PLOT_SCHEMATIC = true; 314 Clipboard blockArrayClipboard3 = schematic3.getClipboard(); 315 316 BlockVector3 d3 = blockArrayClipboard3.getDimensions(); 317 short w3 = (short) d3.getX(); 318 short l3 = (short) d3.getZ(); 319 short h3 = (short) d3.getY(); 320 if (w3 > PLOT_WIDTH || l3 > PLOT_WIDTH) { 321 this.ROAD_SCHEMATIC_ENABLED = true; 322 } 323 int centerShiftZ; 324 if (l3 < this.PLOT_WIDTH) { 325 centerShiftZ = (this.PLOT_WIDTH - l3) / 2; 326 } else { 327 centerShiftZ = (PLOT_WIDTH - l3) / 2; 328 } 329 int centerShiftX; 330 if (w3 < this.PLOT_WIDTH) { 331 centerShiftX = (this.PLOT_WIDTH - w3) / 2; 332 } else { 333 centerShiftX = (PLOT_WIDTH - w3) / 2; 334 } 335 336 BlockVector3 min = blockArrayClipboard3.getMinimumPoint(); 337 for (short x = 0; x < w3; x++) { 338 for (short z = 0; z < l3; z++) { 339 for (short y = 0; y < h3; y++) { 340 BaseBlock id = blockArrayClipboard3.getFullBlock(BlockVector3.at( 341 x + min.getBlockX(), 342 y + min.getBlockY(), 343 z + min.getBlockZ() 344 )); 345 schem3PopulationNeeded |= id.hasNbtData(); 346 addOverlayBlock( 347 (short) (x + shift + oddshift + centerShiftX), 348 (short) (y + plotY), 349 (short) (z + shift + oddshift + centerShiftZ), 350 id, 351 false, 352 maxSchematicHeight 353 ); 354 } 355 if (blockArrayClipboard3.hasBiomes()) { 356 BiomeType biome = blockArrayClipboard3.getBiome(BlockVector2.at( 357 x + min.getBlockX(), 358 z + min.getBlockZ() 359 )); 360 addOverlayBiome( 361 (short) (x + shift + oddshift + centerShiftX), 362 (short) (z + shift + oddshift + centerShiftZ), 363 biome 364 ); 365 } 366 } 367 } 368 369 if (Settings.DEBUG) { 370 LOGGER.info("- plot schematic: {}", schematic3File.getPath()); 371 } 372 } 373 if ((schematic1 == null && schematic2 == null) || this.ROAD_WIDTH == 0) { 374 if (Settings.DEBUG) { 375 LOGGER.info("- road schematic: false"); 376 } 377 return; 378 } 379 this.ROAD_SCHEMATIC_ENABLED = true; 380 // Do not populate road if using schematic population 381 // TODO: What? this.ROAD_BLOCK = BlockBucket.empty(); // BlockState.getEmptyData(this.ROAD_BLOCK); // BlockUtil.get(this.ROAD_BLOCK.id, (byte) 0); 382 383 Clipboard blockArrayClipboard1 = schematic1.getClipboard(); 384 385 BlockVector3 d1 = blockArrayClipboard1.getDimensions(); 386 short w1 = (short) d1.getX(); 387 short l1 = (short) d1.getZ(); 388 short h1 = (short) d1.getY(); 389 // Workaround for schematic height issue if proper calculation of road schematic height is disabled 390 if (!Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT) { 391 h1 += Math.max(ROAD_HEIGHT - WALL_HEIGHT, 0); 392 } 393 394 BlockVector3 min = blockArrayClipboard1.getMinimumPoint(); 395 for (short x = 0; x < w1; x++) { 396 for (short z = 0; z < l1; z++) { 397 for (short y = 0; y < h1; y++) { 398 BaseBlock id = blockArrayClipboard1.getFullBlock(BlockVector3.at( 399 x + min.getBlockX(), 400 y + min.getBlockY(), 401 z + min.getBlockZ() 402 )); 403 schem1PopulationNeeded |= id.hasNbtData(); 404 addOverlayBlock( 405 (short) (x - shift), 406 (short) (y + roadY), 407 (short) (z + shift + oddshift), 408 id, 409 false, 410 maxSchematicHeight 411 ); 412 addOverlayBlock( 413 (short) (z + shift + oddshift), 414 (short) (y + roadY), 415 (short) (shift - x + (oddshift - 1)), 416 id, 417 true, 418 maxSchematicHeight 419 ); 420 } 421 if (blockArrayClipboard1.hasBiomes()) { 422 BiomeType biome = blockArrayClipboard1.getBiome(BlockVector2.at(x + min.getBlockX(), z + min.getBlockZ())); 423 addOverlayBiome((short) (x - shift), (short) (z + shift + oddshift), biome); 424 addOverlayBiome((short) (z + shift + oddshift), (short) (shift - x + (oddshift - 1)), biome); 425 } 426 } 427 } 428 429 Clipboard blockArrayClipboard2 = schematic2.getClipboard(); 430 BlockVector3 d2 = blockArrayClipboard2.getDimensions(); 431 short w2 = (short) d2.getX(); 432 short l2 = (short) d2.getZ(); 433 short h2 = (short) d2.getY(); 434 // Workaround for schematic height issue if proper calculation of road schematic height is disabled 435 if (!Settings.Schematics.USE_WALL_IN_ROAD_SCHEM_HEIGHT) { 436 h2 += Math.max(ROAD_HEIGHT - WALL_HEIGHT, 0); 437 } 438 min = blockArrayClipboard2.getMinimumPoint(); 439 for (short x = 0; x < w2; x++) { 440 for (short z = 0; z < l2; z++) { 441 for (short y = 0; y < h2; y++) { 442 BaseBlock id = blockArrayClipboard2.getFullBlock(BlockVector3.at( 443 x + min.getBlockX(), 444 y + min.getBlockY(), 445 z + min.getBlockZ() 446 )); 447 schem2PopulationNeeded |= id.hasNbtData(); 448 addOverlayBlock( 449 (short) (x - shift), 450 (short) (y + roadY), 451 (short) (z - shift), 452 id, 453 false, 454 maxSchematicHeight 455 ); 456 } 457 if (blockArrayClipboard2.hasBiomes()) { 458 BiomeType biome = blockArrayClipboard2.getBiome(BlockVector2.at(x + min.getBlockX(), z + min.getBlockZ())); 459 addOverlayBiome((short) (x - shift), (short) (z - shift), biome); 460 } 461 } 462 } 463 } 464 465 private void addOverlayBlock(short x, short y, short z, BaseBlock id, boolean rotate, int height) { 466 if (z < 0) { 467 z += this.SIZE; 468 } else if (z >= this.SIZE) { 469 z -= this.SIZE; 470 } 471 if (x < 0) { 472 x += this.SIZE; 473 } else if (x >= this.SIZE) { 474 x -= this.SIZE; 475 } 476 if (rotate) { 477 id = rotate(id); 478 } 479 int pair = MathMan.pair(x, z); 480 BaseBlock[] existing = this.G_SCH.computeIfAbsent(pair, k -> new BaseBlock[height]); 481 if (y >= height) { 482 if (y > lastOverlayHeightError) { 483 lastOverlayHeightError = y; 484 LOGGER.error( 485 "Error adding overlay block in world {}. `y > height`. y={}, height={}", 486 getWorldName(), 487 y, 488 height 489 ); 490 } 491 return; 492 } 493 existing[y] = id; 494 } 495 496 private void addOverlayBiome(short x, short z, BiomeType id) { 497 if (z < 0) { 498 z += this.SIZE; 499 } else if (z >= this.SIZE) { 500 z -= this.SIZE; 501 } 502 if (x < 0) { 503 x += this.SIZE; 504 } else if (x >= this.SIZE) { 505 x -= this.SIZE; 506 } 507 int pair = MathMan.pair(x, z); 508 this.G_SCH_B.put(pair, id); 509 } 510 511 /** 512 * Get the entities contained within the plot schematic for generation. Intended for internal use only. 513 * 514 * @since 6.9.0 515 */ 516 @DoNotUse 517 public @Nullable List<Entity> getPlotSchematicEntities() { 518 return schem3Entities; 519 } 520 521 /** 522 * Get the minimum point of the plot schematic for generation. Intended for internal use only. 523 * 524 * @since 6.9.0 525 */ 526 @DoNotUse 527 public @Nullable BlockVector3 getPlotSchematicMinPoint() { 528 return schem3MinPoint; 529 } 530 531 /** 532 * Get if post-generation population of chunks with tiles/entities is needed for this world. Not for public API use. 533 * 534 * @since 6.9.0 535 */ 536 @DoNotUse 537 public boolean populationNeeded() { 538 return schem1PopulationNeeded || schem2PopulationNeeded || schem3PopulationNeeded; 539 } 540 541 /** 542 * Get the root folder for this world's generation schematics. May be null if schematics not initialised via 543 * {@link HybridPlotWorld#setupSchematics()} 544 * 545 * @since 6.9.0 546 */ 547 public @Nullable File getSchematicRoot() { 548 return this.root; 549 } 550 551 /** 552 * Get the y value where the plot schematic should be pasted from. 553 * 554 * @return plot schematic y start value 555 * @since 7.0.0 556 */ 557 public int getPlotYStart() { 558 return SCHEM_Y + plotY; 559 } 560 561 /** 562 * Get the y value where the road schematic should be pasted from. 563 * 564 * @return road schematic y start value 565 * @since 7.0.0 566 */ 567 public int getRoadYStart() { 568 return SCHEM_Y + roadY; 569 } 570 571}