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.MinecraftVersion; 034import com.plotsquared.core.util.PlayerManager; 035import com.plotsquared.core.util.StringComparison; 036import com.plotsquared.core.util.WorldUtil; 037import com.plotsquared.core.util.task.TaskManager; 038import com.sk89q.worldedit.bukkit.BukkitAdapter; 039import com.sk89q.worldedit.bukkit.BukkitWorld; 040import com.sk89q.worldedit.math.BlockVector2; 041import com.sk89q.worldedit.world.biome.BiomeType; 042import com.sk89q.worldedit.world.block.BlockCategories; 043import com.sk89q.worldedit.world.block.BlockState; 044import com.sk89q.worldedit.world.block.BlockType; 045import com.sk89q.worldedit.world.block.BlockTypes; 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.type.WallSign; 060import org.bukkit.entity.Allay; 061import org.bukkit.entity.Ambient; 062import org.bukkit.entity.Animals; 063import org.bukkit.entity.AreaEffectCloud; 064import org.bukkit.entity.ArmorStand; 065import org.bukkit.entity.Boss; 066import org.bukkit.entity.EnderCrystal; 067import org.bukkit.entity.EnderSignal; 068import org.bukkit.entity.Entity; 069import org.bukkit.entity.EntityType; 070import org.bukkit.entity.EvokerFangs; 071import org.bukkit.entity.ExperienceOrb; 072import org.bukkit.entity.Explosive; 073import org.bukkit.entity.FallingBlock; 074import org.bukkit.entity.Firework; 075import org.bukkit.entity.Ghast; 076import org.bukkit.entity.Hanging; 077import org.bukkit.entity.Interaction; 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 PaperSupport.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 PaperSupport.getChunkAtAsync(adapt(location)).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 public boolean isSmallBlock(Location location) { 266 return adapt(location).getBlock().getBoundingBox().getHeight() < 0.25; 267 } 268 269 @Override 270 @NonNegative 271 public int getHighestBlockSynchronous(final @NonNull String world, final int x, final int z) { 272 final World bukkitWorld = Objects.requireNonNull(getWorld(world)); 273 // Skip top and bottom block 274 int air = 1; 275 int maxY = com.plotsquared.bukkit.util.BukkitWorld.getMaxWorldHeight(bukkitWorld); 276 int minY = com.plotsquared.bukkit.util.BukkitWorld.getMinWorldHeight(bukkitWorld); 277 for (int y = maxY - 1; y >= minY; y--) { 278 Block block = bukkitWorld.getBlockAt(x, y, z); 279 Material type = block.getType(); 280 if (type.isSolid()) { 281 if (air > 1) { 282 return y; 283 } 284 air = 0; 285 } else { 286 if (block.isLiquid()) { 287 return y; 288 } 289 air++; 290 } 291 } 292 return bukkitWorld.getMaxHeight() - 1; 293 } 294 295 @Override 296 public @NonNull String[] getSignSynchronous(final @NonNull Location location) { 297 Block block = Objects.requireNonNull(getWorld(location.getWorldName())).getBlockAt( 298 location.getX(), 299 location.getY(), 300 location.getZ() 301 ); 302 try { 303 return TaskManager.getPlatformImplementation().sync(() -> { 304 if (block.getState() instanceof Sign sign) { 305 return sign.getLines(); 306 } 307 return new String[0]; 308 }); 309 } catch (final Exception e) { 310 e.printStackTrace(); 311 } 312 return new String[0]; 313 } 314 315 @Override 316 public @NonNull Location getSpawn(final @NonNull String world) { 317 final org.bukkit.Location temp = getWorld(world).getSpawnLocation(); 318 return Location.at(world, temp.getBlockX(), temp.getBlockY(), temp.getBlockZ(), temp.getYaw(), temp.getPitch()); 319 } 320 321 @Override 322 public void setSpawn(final @NonNull Location location) { 323 final World world = getWorld(location.getWorldName()); 324 if (world != null) { 325 world.setSpawnLocation(location.getX(), location.getY(), location.getZ()); 326 } 327 } 328 329 @Override 330 public void saveWorld(final @NonNull String worldName) { 331 final World world = getWorld(worldName); 332 if (world != null) { 333 world.save(); 334 } 335 } 336 337 @Override 338 @SuppressWarnings("deprecation") 339 public void setSign( 340 final @NonNull Location location, final @NonNull Caption[] lines, 341 final @NonNull TagResolver... replacements 342 ) { 343 ensureLoaded(location.getWorldName(), location.getX(), location.getZ(), chunk -> { 344 PlotArea area = location.getPlotArea(); 345 final World world = getWorld(location.getWorldName()); 346 final Block block = world.getBlockAt(location.getX(), location.getY(), location.getZ()); 347 final Material type = block.getType(); 348 if (type != Material.LEGACY_SIGN && type != Material.LEGACY_WALL_SIGN) { 349 BlockFace facing = BlockFace.NORTH; 350 if (!world.getBlockAt(location.getX(), location.getY(), location.getZ() + 1).getType().isSolid()) { 351 if (world.getBlockAt(location.getX() - 1, location.getY(), location.getZ()).getType().isSolid()) { 352 facing = BlockFace.EAST; 353 } else if (world.getBlockAt(location.getX() + 1, location.getY(), location.getZ()).getType().isSolid()) { 354 facing = BlockFace.WEST; 355 } else if (world.getBlockAt(location.getX(), location.getY(), location.getZ() - 1).getType().isSolid()) { 356 facing = BlockFace.SOUTH; 357 } 358 } 359 if (MinecraftVersion.current().isOlderOrEqualThan(13)) { 360 block.setType(Material.valueOf(area.legacySignMaterial()), false); 361 } else { 362 block.setType(Material.valueOf(area.signMaterial()), false); 363 } 364 if (!(block.getBlockData() instanceof WallSign sign)) { 365 throw new RuntimeException("Something went wrong generating a sign"); 366 } 367 sign.setFacing(facing); 368 block.setBlockData(sign, false); 369 } 370 final org.bukkit.block.BlockState blockstate = block.getState(); 371 if (blockstate instanceof final Sign sign) { 372 for (int i = 0; i < lines.length; i++) { 373 sign.setLine(i, LEGACY_COMPONENT_SERIALIZER.serialize( 374 MINI_MESSAGE.deserialize(lines[i].getComponent(LocaleHolder.console()), replacements) 375 )); 376 } 377 sign.update(true, false); 378 } 379 }); 380 } 381 382 @Override 383 public @NonNull StringComparison<BlockState>.ComparisonResult getClosestBlock(@NonNull String name) { 384 BlockState state = BlockUtil.get(name); 385 return new StringComparison<BlockState>().new ComparisonResult(1, state); 386 } 387 388 @Override 389 public com.sk89q.worldedit.world.@NonNull World getWeWorld(final @NonNull String world) { 390 return new BukkitWorld(Bukkit.getWorld(world)); 391 } 392 393 @Override 394 public void refreshChunk(int x, int z, String world) { 395 Bukkit.getWorld(world).refreshChunk(x, z); 396 } 397 398 @Override 399 public void getBlock(final @NonNull Location location, final @NonNull Consumer<BlockState> result) { 400 ensureLoaded(location, chunk -> { 401 final World world = getWorld(location.getWorldName()); 402 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 403 result.accept(Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState()); 404 }); 405 } 406 407 @Override 408 public @NonNull BlockState getBlockSynchronous(final @NonNull Location location) { 409 final World world = getWorld(location.getWorldName()); 410 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 411 return Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState(); 412 } 413 414 @Override 415 @NonNegative 416 public double getHealth(final @NonNull PlotPlayer<?> player) { 417 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getHealth(); 418 } 419 420 @Override 421 @NonNegative 422 public int getFoodLevel(final @NonNull PlotPlayer<?> player) { 423 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getFoodLevel(); 424 } 425 426 @Override 427 public void setHealth(final @NonNull PlotPlayer<?> player, @NonNegative final double health) { 428 Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).setHealth(health); 429 } 430 431 @Override 432 public void setFoodLevel(final @NonNull PlotPlayer<?> player, @NonNegative final int foodLevel) { 433 Bukkit.getPlayer(player.getUUID()).setFoodLevel(foodLevel); 434 } 435 436 @Override 437 public @NonNull Set<com.sk89q.worldedit.world.entity.EntityType> getTypesInCategory(final @NonNull String category) { 438 final Collection<Class<?>> allowedInterfaces = new HashSet<>(); 439 switch (category) { 440 case "animal" -> { 441 allowedInterfaces.add(IronGolem.class); 442 allowedInterfaces.add(Snowman.class); 443 allowedInterfaces.add(Animals.class); 444 allowedInterfaces.add(WaterMob.class); 445 allowedInterfaces.add(Ambient.class); 446 if (MinecraftVersion.current().isOlderOrEqualThan(MinecraftVersion.THE_WILD_UPDATE)) { 447 allowedInterfaces.add(Allay.class); 448 } 449 } 450 case "tameable" -> allowedInterfaces.add(Tameable.class); 451 case "vehicle" -> allowedInterfaces.add(Vehicle.class); 452 case "hostile" -> { 453 allowedInterfaces.add(Shulker.class); 454 allowedInterfaces.add(Monster.class); 455 allowedInterfaces.add(Boss.class); 456 allowedInterfaces.add(Slime.class); 457 allowedInterfaces.add(Ghast.class); 458 allowedInterfaces.add(Phantom.class); 459 allowedInterfaces.add(EnderCrystal.class); 460 } 461 case "hanging" -> allowedInterfaces.add(Hanging.class); 462 case "villager" -> allowedInterfaces.add(NPC.class); 463 case "projectile" -> allowedInterfaces.add(Projectile.class); 464 case "other" -> { 465 allowedInterfaces.add(ArmorStand.class); 466 allowedInterfaces.add(FallingBlock.class); 467 allowedInterfaces.add(Item.class); 468 allowedInterfaces.add(Explosive.class); 469 allowedInterfaces.add(AreaEffectCloud.class); 470 allowedInterfaces.add(EvokerFangs.class); 471 allowedInterfaces.add(LightningStrike.class); 472 allowedInterfaces.add(ExperienceOrb.class); 473 allowedInterfaces.add(EnderSignal.class); 474 allowedInterfaces.add(Firework.class); 475 } 476 case "player" -> allowedInterfaces.add(Player.class); 477 case "interaction" -> { 478 if (MinecraftVersion.current().isNewerOrEqualThan(19, 4)) { 479 allowedInterfaces.add(Interaction.class); 480 } 481 } 482 default -> LOGGER.error("Unknown entity category requested: {}", category); 483 } 484 final Set<com.sk89q.worldedit.world.entity.EntityType> types = new HashSet<>(); 485 outer: 486 for (final EntityType bukkitType : EntityType.values()) { 487 final Class<? extends Entity> entityClass = bukkitType.getEntityClass(); 488 if (entityClass == null) { 489 continue; 490 } 491 for (final Class<?> allowedInterface : allowedInterfaces) { 492 if (allowedInterface.isAssignableFrom(entityClass)) { 493 types.add(BukkitAdapter.adapt(bukkitType)); 494 continue outer; 495 } 496 } 497 } 498 return types; 499 } 500 501 @Override 502 public @NonNull Collection<BlockType> getTileEntityTypes() { 503 if (this.tileEntityTypes.isEmpty()) { 504 // Categories 505 tileEntityTypes.addAll(BlockCategories.BANNERS.getAll()); 506 tileEntityTypes.addAll(BlockCategories.SIGNS.getAll()); 507 tileEntityTypes.addAll(BlockCategories.BEDS.getAll()); 508 tileEntityTypes.addAll(BlockCategories.FLOWER_POTS.getAll()); 509 // Individual Types 510 // Add these from strings 511 Stream.of( 512 "barrel", 513 "beacon", 514 "beehive", 515 "bee_nest", 516 "bell", 517 "blast_furnace", 518 "brewing_stand", 519 "campfire", 520 "chest", 521 "ender_chest", 522 "trapped_chest", 523 "command_block", 524 "end_gateway", 525 "hopper", 526 "jigsaw", 527 "jubekox", 528 "lectern", 529 "note_block", 530 "black_shulker_box", 531 "blue_shulker_box", 532 "brown_shulker_box", 533 "cyan_shulker_box", 534 "gray_shulker_box", 535 "green_shulker_box", 536 "light_blue_shulker_box", 537 "light_gray_shulker_box", 538 "lime_shulker_box", 539 "magenta_shulker_box", 540 "orange_shulker_box", 541 "pink_shulker_box", 542 "purple_shulker_box", 543 "red_shulker_box", 544 "shulker_box", 545 "white_shulker_box", 546 "yellow_shulker_box", 547 "smoker", 548 "structure_block", 549 "structure_void" 550 ) 551 .map(BlockTypes::get).filter(Objects::nonNull).forEach(tileEntityTypes::add); 552 } 553 return this.tileEntityTypes; 554 } 555 556 @Override 557 @NonNegative 558 public int getTileEntityCount(final @NonNull String world, final @NonNull BlockVector2 chunk) { 559 return Objects.requireNonNull(getWorld(world)). 560 getChunkAt(chunk.getBlockX(), chunk.getBlockZ()).getTileEntities().length; 561 } 562 563 @Override 564 public Set<BlockVector2> getChunkChunks(com.plotsquared.core.location.World<?> world) { 565 Set<BlockVector2> chunks = super.getChunkChunks(world); 566 World bukkitWorld = ((com.plotsquared.bukkit.util.BukkitWorld) world).getPlatformWorld(); 567 if (Bukkit.isPrimaryThread()) { 568 for (Chunk chunk : bukkitWorld.getLoadedChunks()) { 569 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 570 chunks.add(loc); 571 } 572 } else { 573 final Semaphore semaphore = new Semaphore(1); 574 try { 575 semaphore.acquire(); 576 Bukkit.getScheduler().runTask(BukkitPlatform.getPlugin(BukkitPlatform.class), () -> { 577 for (Chunk chunk : bukkitWorld.getLoadedChunks()) { 578 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 579 chunks.add(loc); 580 } 581 semaphore.release(); 582 }); 583 semaphore.acquireUninterruptibly(); 584 } catch (final Exception e) { 585 e.printStackTrace(); 586 } 587 } 588 return chunks; 589 } 590 591}