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