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.util; 020 021import com.google.inject.Singleton; 022import com.plotsquared.bukkit.BukkitPlatform; 023import com.plotsquared.bukkit.player.BukkitPlayer; 024import com.plotsquared.bukkit.player.BukkitPlayerManager; 025import com.plotsquared.core.PlotSquared; 026import com.plotsquared.core.configuration.caption.Caption; 027import com.plotsquared.core.configuration.caption.LocaleHolder; 028import com.plotsquared.core.location.Location; 029import com.plotsquared.core.player.PlotPlayer; 030import com.plotsquared.core.plot.PlotArea; 031import com.plotsquared.core.util.BlockUtil; 032import com.plotsquared.core.util.MathMan; 033import com.plotsquared.core.util.PlayerManager; 034import com.plotsquared.core.util.StringComparison; 035import com.plotsquared.core.util.WorldUtil; 036import com.plotsquared.core.util.task.TaskManager; 037import com.sk89q.worldedit.bukkit.BukkitAdapter; 038import com.sk89q.worldedit.bukkit.BukkitWorld; 039import com.sk89q.worldedit.math.BlockVector2; 040import com.sk89q.worldedit.world.biome.BiomeType; 041import com.sk89q.worldedit.world.block.BlockCategories; 042import com.sk89q.worldedit.world.block.BlockState; 043import com.sk89q.worldedit.world.block.BlockType; 044import com.sk89q.worldedit.world.block.BlockTypes; 045import io.papermc.lib.PaperLib; 046import net.kyori.adventure.platform.bukkit.BukkitAudiences; 047import net.kyori.adventure.text.minimessage.MiniMessage; 048import net.kyori.adventure.text.minimessage.Template; 049import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 050import org.apache.logging.log4j.LogManager; 051import org.apache.logging.log4j.Logger; 052import org.bukkit.Bukkit; 053import org.bukkit.Chunk; 054import org.bukkit.Material; 055import org.bukkit.World; 056import org.bukkit.block.Block; 057import org.bukkit.block.BlockFace; 058import org.bukkit.block.Sign; 059import org.bukkit.block.data.Directional; 060import org.bukkit.block.data.type.WallSign; 061import org.bukkit.entity.Allay; 062import org.bukkit.entity.Ambient; 063import org.bukkit.entity.Animals; 064import org.bukkit.entity.AreaEffectCloud; 065import org.bukkit.entity.ArmorStand; 066import org.bukkit.entity.Boss; 067import org.bukkit.entity.EnderCrystal; 068import org.bukkit.entity.EnderSignal; 069import org.bukkit.entity.Entity; 070import org.bukkit.entity.EntityType; 071import org.bukkit.entity.EvokerFangs; 072import org.bukkit.entity.ExperienceOrb; 073import org.bukkit.entity.Explosive; 074import org.bukkit.entity.FallingBlock; 075import org.bukkit.entity.Firework; 076import org.bukkit.entity.Ghast; 077import org.bukkit.entity.Hanging; 078import org.bukkit.entity.IronGolem; 079import org.bukkit.entity.Item; 080import org.bukkit.entity.LightningStrike; 081import org.bukkit.entity.Monster; 082import org.bukkit.entity.NPC; 083import org.bukkit.entity.Phantom; 084import org.bukkit.entity.Player; 085import org.bukkit.entity.Projectile; 086import org.bukkit.entity.Shulker; 087import org.bukkit.entity.Slime; 088import org.bukkit.entity.Snowman; 089import org.bukkit.entity.Tameable; 090import org.bukkit.entity.Vehicle; 091import org.bukkit.entity.WaterMob; 092import org.checkerframework.checker.index.qual.NonNegative; 093import org.checkerframework.checker.nullness.qual.NonNull; 094import org.checkerframework.checker.nullness.qual.Nullable; 095 096import java.util.Collection; 097import java.util.HashSet; 098import java.util.Objects; 099import java.util.Set; 100import java.util.concurrent.Semaphore; 101import java.util.function.Consumer; 102import java.util.function.IntConsumer; 103import java.util.stream.Stream; 104 105@SuppressWarnings({"unused", "WeakerAccess"}) 106@Singleton 107public class BukkitUtil extends WorldUtil { 108 109 public static final BukkitAudiences BUKKIT_AUDIENCES = BukkitAudiences.create(BukkitPlatform.getPlugin(BukkitPlatform.class)); 110 public static final LegacyComponentSerializer LEGACY_COMPONENT_SERIALIZER = LegacyComponentSerializer.legacySection(); 111 public static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build(); 112 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + BukkitUtil.class.getSimpleName()); 113 private final Collection<BlockType> tileEntityTypes = new HashSet<>(); 114 115 /** 116 * Turn a Bukkit {@link Player} into a PlotSquared {@link PlotPlayer} 117 * 118 * @param player Bukkit player 119 * @return PlotSquared player 120 */ 121 public static @NonNull BukkitPlayer adapt(final @NonNull Player player) { 122 final PlayerManager<?, ?> playerManager = PlotSquared.platform().playerManager(); 123 return ((BukkitPlayerManager) playerManager).getPlayer(player); 124 } 125 126 /** 127 * Turn a Bukkit {@link org.bukkit.Location} into a PlotSquared {@link Location}. 128 * This only copies the 4-tuple (world,x,y,z) and does not include the yaw and the pitch 129 * 130 * @param location Bukkit location 131 * @return PlotSquared location 132 */ 133 public static @NonNull Location adapt(final org.bukkit.@NonNull Location location) { 134 return Location 135 .at( 136 com.plotsquared.bukkit.util.BukkitWorld.of(location.getWorld()), 137 MathMan.roundInt(location.getX()), 138 MathMan.roundInt(location.getY()), 139 MathMan.roundInt(location.getZ()) 140 ); 141 } 142 143 /** 144 * Turn a Bukkit {@link org.bukkit.Location} into a PlotSquared {@link Location}. 145 * This copies the entire 6-tuple (world,x,y,z,yaw,pitch). 146 * 147 * @param location Bukkit location 148 * @return PlotSquared location 149 */ 150 public static @NonNull Location adaptComplete(final org.bukkit.@NonNull Location location) { 151 return Location 152 .at( 153 com.plotsquared.bukkit.util.BukkitWorld.of(location.getWorld()), 154 MathMan.roundInt(location.getX()), 155 MathMan.roundInt(location.getY()), 156 MathMan.roundInt(location.getZ()), 157 location.getYaw(), 158 location.getPitch() 159 ); 160 } 161 162 /** 163 * Turn a PlotSquared {@link Location} into a Bukkit {@link org.bukkit.Location}. 164 * This only copies the 4-tuple (world,x,y,z) and does not include the yaw and the pitch 165 * 166 * @param location PlotSquared location 167 * @return Bukkit location 168 */ 169 public static org.bukkit.@NonNull Location adapt(final @NonNull Location location) { 170 return new org.bukkit.Location( 171 (World) location.getWorld().getPlatformWorld(), 172 location.getX(), 173 location.getY(), 174 location.getZ() 175 ); 176 } 177 178 /** 179 * Get a Bukkit {@link World} from its name 180 * 181 * @param string World name 182 * @return World if it exists, or {@code null} 183 */ 184 public static @Nullable World getWorld(final @NonNull String string) { 185 return Bukkit.getWorld(string); 186 } 187 188 private static void ensureLoaded( 189 final @NonNull String world, 190 final int x, 191 final int z, 192 final @NonNull Consumer<Chunk> chunkConsumer 193 ) { 194 PaperLib.getChunkAtAsync(Objects.requireNonNull(getWorld(world)), x >> 4, z >> 4, true) 195 .thenAccept(chunk -> ensureMainThread(chunkConsumer, chunk)); 196 } 197 198 private static void ensureLoaded(final @NonNull Location location, final @NonNull Consumer<Chunk> chunkConsumer) { 199 PaperLib.getChunkAtAsync(adapt(location), true).thenAccept(chunk -> ensureMainThread(chunkConsumer, chunk)); 200 } 201 202 private static <T> void ensureMainThread(final @NonNull Consumer<T> consumer, final @NonNull T value) { 203 if (Bukkit.isPrimaryThread()) { 204 consumer.accept(value); 205 } else { 206 Bukkit.getScheduler().runTask(BukkitPlatform.getPlugin(BukkitPlatform.class), () -> consumer.accept(value)); 207 } 208 } 209 210 @Override 211 public boolean isBlockSame(final @NonNull BlockState block1, final @NonNull BlockState block2) { 212 if (block1.equals(block2)) { 213 return true; 214 } 215 final Material mat1 = BukkitAdapter.adapt(block1.getBlockType()); 216 final Material mat2 = BukkitAdapter.adapt(block2.getBlockType()); 217 return mat1 == mat2; 218 } 219 220 @Override 221 public boolean isWorld(final @NonNull String worldName) { 222 return getWorld(worldName) != null; 223 } 224 225 @Override 226 public void getBiome(final @NonNull String world, final int x, final int z, final @NonNull Consumer<BiomeType> result) { 227 ensureLoaded(world, x, z, chunk -> result.accept(BukkitAdapter.adapt(getWorld(world).getBiome(x, z)))); 228 } 229 230 @Override 231 public @NonNull BiomeType getBiomeSynchronous(final @NonNull String world, final int x, final int z) { 232 return BukkitAdapter.adapt(Objects.requireNonNull(getWorld(world)).getBiome(x, z)); 233 } 234 235 @Override 236 public void getHighestBlock(final @NonNull String world, final int x, final int z, final @NonNull IntConsumer result) { 237 ensureLoaded(world, x, z, chunk -> { 238 final World bukkitWorld = Objects.requireNonNull(getWorld(world)); 239 // Skip top and bottom block 240 int air = 1; 241 int maxY = com.plotsquared.bukkit.util.BukkitWorld.getMaxWorldHeight(bukkitWorld); 242 int minY = com.plotsquared.bukkit.util.BukkitWorld.getMinWorldHeight(bukkitWorld); 243 for (int y = maxY - 1; y >= minY; y--) { 244 Block block = bukkitWorld.getBlockAt(x, y, z); 245 Material type = block.getType(); 246 if (type.isSolid()) { 247 if (air > 1) { 248 result.accept(y); 249 return; 250 } 251 air = 0; 252 } else { 253 if (block.isLiquid()) { 254 result.accept(y); 255 return; 256 } 257 air++; 258 } 259 } 260 result.accept(bukkitWorld.getMaxHeight() - 1); 261 }); 262 } 263 264 @Override 265 @NonNegative 266 public int getHighestBlockSynchronous(final @NonNull String world, final int x, final int z) { 267 final World bukkitWorld = Objects.requireNonNull(getWorld(world)); 268 // Skip top and bottom block 269 int air = 1; 270 int maxY = com.plotsquared.bukkit.util.BukkitWorld.getMaxWorldHeight(bukkitWorld); 271 int minY = com.plotsquared.bukkit.util.BukkitWorld.getMinWorldHeight(bukkitWorld); 272 for (int y = maxY - 1; y >= minY; y--) { 273 Block block = bukkitWorld.getBlockAt(x, y, z); 274 Material type = block.getType(); 275 if (type.isSolid()) { 276 if (air > 1) { 277 return y; 278 } 279 air = 0; 280 } else { 281 if (block.isLiquid()) { 282 return y; 283 } 284 air++; 285 } 286 } 287 return bukkitWorld.getMaxHeight() - 1; 288 } 289 290 @Override 291 public @NonNull String[] getSignSynchronous(final @NonNull Location location) { 292 Block block = Objects.requireNonNull(getWorld(location.getWorldName())).getBlockAt( 293 location.getX(), 294 location.getY(), 295 location.getZ() 296 ); 297 try { 298 return TaskManager.getPlatformImplementation().sync(() -> { 299 if (block.getState() instanceof Sign sign) { 300 return sign.getLines(); 301 } 302 return new String[0]; 303 }); 304 } catch (final Exception e) { 305 e.printStackTrace(); 306 } 307 return new String[0]; 308 } 309 310 @Override 311 public @NonNull Location getSpawn(final @NonNull String world) { 312 final org.bukkit.Location temp = getWorld(world).getSpawnLocation(); 313 return Location.at(world, temp.getBlockX(), temp.getBlockY(), temp.getBlockZ(), temp.getYaw(), temp.getPitch()); 314 } 315 316 @Override 317 public void setSpawn(final @NonNull Location location) { 318 final World world = getWorld(location.getWorldName()); 319 if (world != null) { 320 world.setSpawnLocation(location.getX(), location.getY(), location.getZ()); 321 } 322 } 323 324 @Override 325 public void saveWorld(final @NonNull String worldName) { 326 final World world = getWorld(worldName); 327 if (world != null) { 328 world.save(); 329 } 330 } 331 332 @Override 333 @SuppressWarnings("deprecation") 334 public void setSign( 335 final @NonNull Location location, final @NonNull Caption[] lines, 336 final @NonNull Template... replacements 337 ) { 338 ensureLoaded(location.getWorldName(), location.getX(), location.getZ(), chunk -> { 339 PlotArea area = location.getPlotArea(); 340 final World world = getWorld(location.getWorldName()); 341 final Block block = world.getBlockAt(location.getX(), location.getY(), location.getZ()); 342 final Material type = block.getType(); 343 if (type != Material.LEGACY_SIGN && type != Material.LEGACY_WALL_SIGN) { 344 BlockFace facing = BlockFace.NORTH; 345 if (!world.getBlockAt(location.getX(), location.getY(), location.getZ() + 1).getType().isSolid()) { 346 if (world.getBlockAt(location.getX() - 1, location.getY(), location.getZ()).getType().isSolid()) { 347 facing = BlockFace.EAST; 348 } else if (world.getBlockAt(location.getX() + 1, location.getY(), location.getZ()).getType().isSolid()) { 349 facing = BlockFace.WEST; 350 } else if (world.getBlockAt(location.getX(), location.getY(), location.getZ() - 1).getType().isSolid()) { 351 facing = BlockFace.SOUTH; 352 } 353 } 354 if (PlotSquared.platform().serverVersion()[1] == 13) { 355 block.setType(Material.valueOf(area.legacySignMaterial()), false); 356 } else { 357 block.setType(Material.valueOf(area.signMaterial()), false); 358 } 359 if (!(block.getBlockData() instanceof WallSign)) { 360 throw new RuntimeException("Something went wrong generating a sign"); 361 } 362 final Directional sign = (Directional) block.getBlockData(); 363 sign.setFacing(facing); 364 block.setBlockData(sign, false); 365 } 366 final org.bukkit.block.BlockState blockstate = block.getState(); 367 if (blockstate instanceof final Sign sign) { 368 for (int i = 0; i < lines.length; i++) { 369 sign.setLine(i, LEGACY_COMPONENT_SERIALIZER 370 .serialize(MINI_MESSAGE.parse(lines[i].getComponent(LocaleHolder.console()), replacements))); 371 } 372 sign.update(true, false); 373 } 374 }); 375 } 376 377 @Override 378 public @NonNull StringComparison<BlockState>.ComparisonResult getClosestBlock(@NonNull String name) { 379 BlockState state = BlockUtil.get(name); 380 return new StringComparison<BlockState>().new ComparisonResult(1, state); 381 } 382 383 @Override 384 public com.sk89q.worldedit.world.@NonNull World getWeWorld(final @NonNull String world) { 385 return new BukkitWorld(Bukkit.getWorld(world)); 386 } 387 388 @Override 389 public void refreshChunk(int x, int z, String world) { 390 Bukkit.getWorld(world).refreshChunk(x, z); 391 } 392 393 @Override 394 public void getBlock(final @NonNull Location location, final @NonNull Consumer<BlockState> result) { 395 ensureLoaded(location, chunk -> { 396 final World world = getWorld(location.getWorldName()); 397 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 398 result.accept(Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState()); 399 }); 400 } 401 402 @Override 403 public @NonNull BlockState getBlockSynchronous(final @NonNull Location location) { 404 final World world = getWorld(location.getWorldName()); 405 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 406 return Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState(); 407 } 408 409 @Override 410 @NonNegative 411 public double getHealth(final @NonNull PlotPlayer<?> player) { 412 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getHealth(); 413 } 414 415 @Override 416 @NonNegative 417 public int getFoodLevel(final @NonNull PlotPlayer<?> player) { 418 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getFoodLevel(); 419 } 420 421 @Override 422 public void setHealth(final @NonNull PlotPlayer<?> player, @NonNegative final double health) { 423 Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).setHealth(health); 424 } 425 426 @Override 427 public void setFoodLevel(final @NonNull PlotPlayer<?> player, @NonNegative final int foodLevel) { 428 Bukkit.getPlayer(player.getUUID()).setFoodLevel(foodLevel); 429 } 430 431 @Override 432 public @NonNull Set<com.sk89q.worldedit.world.entity.EntityType> getTypesInCategory(final @NonNull String category) { 433 final Collection<Class<?>> allowedInterfaces = new HashSet<>(); 434 switch (category) { 435 case "animal" -> { 436 allowedInterfaces.add(IronGolem.class); 437 allowedInterfaces.add(Snowman.class); 438 allowedInterfaces.add(Animals.class); 439 allowedInterfaces.add(WaterMob.class); 440 allowedInterfaces.add(Ambient.class); 441 if (PlotSquared.platform().serverVersion()[1] >= 19) { 442 allowedInterfaces.add(Allay.class); 443 } 444 } 445 case "tameable" -> allowedInterfaces.add(Tameable.class); 446 case "vehicle" -> allowedInterfaces.add(Vehicle.class); 447 case "hostile" -> { 448 allowedInterfaces.add(Shulker.class); 449 allowedInterfaces.add(Monster.class); 450 allowedInterfaces.add(Boss.class); 451 allowedInterfaces.add(Slime.class); 452 allowedInterfaces.add(Ghast.class); 453 allowedInterfaces.add(Phantom.class); 454 allowedInterfaces.add(EnderCrystal.class); 455 } 456 case "hanging" -> allowedInterfaces.add(Hanging.class); 457 case "villager" -> allowedInterfaces.add(NPC.class); 458 case "projectile" -> allowedInterfaces.add(Projectile.class); 459 case "other" -> { 460 allowedInterfaces.add(ArmorStand.class); 461 allowedInterfaces.add(FallingBlock.class); 462 allowedInterfaces.add(Item.class); 463 allowedInterfaces.add(Explosive.class); 464 allowedInterfaces.add(AreaEffectCloud.class); 465 allowedInterfaces.add(EvokerFangs.class); 466 allowedInterfaces.add(LightningStrike.class); 467 allowedInterfaces.add(ExperienceOrb.class); 468 allowedInterfaces.add(EnderSignal.class); 469 allowedInterfaces.add(Firework.class); 470 } 471 case "player" -> allowedInterfaces.add(Player.class); 472 default -> LOGGER.error("Unknown entity category requested: {}", category); 473 } 474 final Set<com.sk89q.worldedit.world.entity.EntityType> types = new HashSet<>(); 475 outer: 476 for (final EntityType bukkitType : EntityType.values()) { 477 final Class<? extends Entity> entityClass = bukkitType.getEntityClass(); 478 if (entityClass == null) { 479 continue; 480 } 481 for (final Class<?> allowedInterface : allowedInterfaces) { 482 if (allowedInterface.isAssignableFrom(entityClass)) { 483 types.add(BukkitAdapter.adapt(bukkitType)); 484 continue outer; 485 } 486 } 487 } 488 return types; 489 } 490 491 @Override 492 public @NonNull Collection<BlockType> getTileEntityTypes() { 493 if (this.tileEntityTypes.isEmpty()) { 494 // Categories 495 tileEntityTypes.addAll(BlockCategories.BANNERS.getAll()); 496 tileEntityTypes.addAll(BlockCategories.SIGNS.getAll()); 497 tileEntityTypes.addAll(BlockCategories.BEDS.getAll()); 498 tileEntityTypes.addAll(BlockCategories.FLOWER_POTS.getAll()); 499 // Individual Types 500 // Add these from strings 501 Stream.of( 502 "barrel", 503 "beacon", 504 "beehive", 505 "bee_nest", 506 "bell", 507 "blast_furnace", 508 "brewing_stand", 509 "campfire", 510 "chest", 511 "ender_chest", 512 "trapped_chest", 513 "command_block", 514 "end_gateway", 515 "hopper", 516 "jigsaw", 517 "jubekox", 518 "lectern", 519 "note_block", 520 "black_shulker_box", 521 "blue_shulker_box", 522 "brown_shulker_box", 523 "cyan_shulker_box", 524 "gray_shulker_box", 525 "green_shulker_box", 526 "light_blue_shulker_box", 527 "light_gray_shulker_box", 528 "lime_shulker_box", 529 "magenta_shulker_box", 530 "orange_shulker_box", 531 "pink_shulker_box", 532 "purple_shulker_box", 533 "red_shulker_box", 534 "shulker_box", 535 "white_shulker_box", 536 "yellow_shulker_box", 537 "smoker", 538 "structure_block", 539 "structure_void" 540 ) 541 .map(BlockTypes::get).filter(Objects::nonNull).forEach(tileEntityTypes::add); 542 } 543 return this.tileEntityTypes; 544 } 545 546 @Override 547 @NonNegative 548 public int getTileEntityCount(final @NonNull String world, final @NonNull BlockVector2 chunk) { 549 return Objects.requireNonNull(getWorld(world)). 550 getChunkAt(chunk.getBlockX(), chunk.getBlockZ()).getTileEntities().length; 551 } 552 553 @Override 554 public Set<BlockVector2> getChunkChunks(String world) { 555 Set<BlockVector2> chunks = super.getChunkChunks(world); 556 if (Bukkit.isPrimaryThread()) { 557 for (Chunk chunk : Objects.requireNonNull(Bukkit.getWorld(world)).getLoadedChunks()) { 558 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 559 chunks.add(loc); 560 } 561 } else { 562 final Semaphore semaphore = new Semaphore(1); 563 try { 564 semaphore.acquire(); 565 Bukkit.getScheduler().runTask(BukkitPlatform.getPlugin(BukkitPlatform.class), () -> { 566 for (Chunk chunk : Objects.requireNonNull(Bukkit.getWorld(world)).getLoadedChunks()) { 567 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 568 chunks.add(loc); 569 } 570 semaphore.release(); 571 }); 572 semaphore.acquireUninterruptibly(); 573 } catch (final Exception e) { 574 e.printStackTrace(); 575 } 576 } 577 return chunks; 578 } 579 580}