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.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 TagResolver... 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.serialize( 370 MINI_MESSAGE.deserialize(lines[i].getComponent(LocaleHolder.console()), replacements) 371 )); 372 } 373 sign.update(true, false); 374 } 375 }); 376 } 377 378 @Override 379 public @NonNull StringComparison<BlockState>.ComparisonResult getClosestBlock(@NonNull String name) { 380 BlockState state = BlockUtil.get(name); 381 return new StringComparison<BlockState>().new ComparisonResult(1, state); 382 } 383 384 @Override 385 public com.sk89q.worldedit.world.@NonNull World getWeWorld(final @NonNull String world) { 386 return new BukkitWorld(Bukkit.getWorld(world)); 387 } 388 389 @Override 390 public void refreshChunk(int x, int z, String world) { 391 Bukkit.getWorld(world).refreshChunk(x, z); 392 } 393 394 @Override 395 public void getBlock(final @NonNull Location location, final @NonNull Consumer<BlockState> result) { 396 ensureLoaded(location, chunk -> { 397 final World world = getWorld(location.getWorldName()); 398 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 399 result.accept(Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState()); 400 }); 401 } 402 403 @Override 404 public @NonNull BlockState getBlockSynchronous(final @NonNull Location location) { 405 final World world = getWorld(location.getWorldName()); 406 final Block block = Objects.requireNonNull(world).getBlockAt(location.getX(), location.getY(), location.getZ()); 407 return Objects.requireNonNull(BukkitAdapter.asBlockType(block.getType())).getDefaultState(); 408 } 409 410 @Override 411 @NonNegative 412 public double getHealth(final @NonNull PlotPlayer<?> player) { 413 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getHealth(); 414 } 415 416 @Override 417 @NonNegative 418 public int getFoodLevel(final @NonNull PlotPlayer<?> player) { 419 return Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).getFoodLevel(); 420 } 421 422 @Override 423 public void setHealth(final @NonNull PlotPlayer<?> player, @NonNegative final double health) { 424 Objects.requireNonNull(Bukkit.getPlayer(player.getUUID())).setHealth(health); 425 } 426 427 @Override 428 public void setFoodLevel(final @NonNull PlotPlayer<?> player, @NonNegative final int foodLevel) { 429 Bukkit.getPlayer(player.getUUID()).setFoodLevel(foodLevel); 430 } 431 432 @Override 433 public @NonNull Set<com.sk89q.worldedit.world.entity.EntityType> getTypesInCategory(final @NonNull String category) { 434 final Collection<Class<?>> allowedInterfaces = new HashSet<>(); 435 switch (category) { 436 case "animal" -> { 437 allowedInterfaces.add(IronGolem.class); 438 allowedInterfaces.add(Snowman.class); 439 allowedInterfaces.add(Animals.class); 440 allowedInterfaces.add(WaterMob.class); 441 allowedInterfaces.add(Ambient.class); 442 if (PlotSquared.platform().serverVersion()[1] >= 19) { 443 allowedInterfaces.add(Allay.class); 444 } 445 } 446 case "tameable" -> allowedInterfaces.add(Tameable.class); 447 case "vehicle" -> allowedInterfaces.add(Vehicle.class); 448 case "hostile" -> { 449 allowedInterfaces.add(Shulker.class); 450 allowedInterfaces.add(Monster.class); 451 allowedInterfaces.add(Boss.class); 452 allowedInterfaces.add(Slime.class); 453 allowedInterfaces.add(Ghast.class); 454 allowedInterfaces.add(Phantom.class); 455 allowedInterfaces.add(EnderCrystal.class); 456 } 457 case "hanging" -> allowedInterfaces.add(Hanging.class); 458 case "villager" -> allowedInterfaces.add(NPC.class); 459 case "projectile" -> allowedInterfaces.add(Projectile.class); 460 case "other" -> { 461 allowedInterfaces.add(ArmorStand.class); 462 allowedInterfaces.add(FallingBlock.class); 463 allowedInterfaces.add(Item.class); 464 allowedInterfaces.add(Explosive.class); 465 allowedInterfaces.add(AreaEffectCloud.class); 466 allowedInterfaces.add(EvokerFangs.class); 467 allowedInterfaces.add(LightningStrike.class); 468 allowedInterfaces.add(ExperienceOrb.class); 469 allowedInterfaces.add(EnderSignal.class); 470 allowedInterfaces.add(Firework.class); 471 } 472 case "player" -> allowedInterfaces.add(Player.class); 473 default -> LOGGER.error("Unknown entity category requested: {}", category); 474 } 475 final Set<com.sk89q.worldedit.world.entity.EntityType> types = new HashSet<>(); 476 outer: 477 for (final EntityType bukkitType : EntityType.values()) { 478 final Class<? extends Entity> entityClass = bukkitType.getEntityClass(); 479 if (entityClass == null) { 480 continue; 481 } 482 for (final Class<?> allowedInterface : allowedInterfaces) { 483 if (allowedInterface.isAssignableFrom(entityClass)) { 484 types.add(BukkitAdapter.adapt(bukkitType)); 485 continue outer; 486 } 487 } 488 } 489 return types; 490 } 491 492 @Override 493 public @NonNull Collection<BlockType> getTileEntityTypes() { 494 if (this.tileEntityTypes.isEmpty()) { 495 // Categories 496 tileEntityTypes.addAll(BlockCategories.BANNERS.getAll()); 497 tileEntityTypes.addAll(BlockCategories.SIGNS.getAll()); 498 tileEntityTypes.addAll(BlockCategories.BEDS.getAll()); 499 tileEntityTypes.addAll(BlockCategories.FLOWER_POTS.getAll()); 500 // Individual Types 501 // Add these from strings 502 Stream.of( 503 "barrel", 504 "beacon", 505 "beehive", 506 "bee_nest", 507 "bell", 508 "blast_furnace", 509 "brewing_stand", 510 "campfire", 511 "chest", 512 "ender_chest", 513 "trapped_chest", 514 "command_block", 515 "end_gateway", 516 "hopper", 517 "jigsaw", 518 "jubekox", 519 "lectern", 520 "note_block", 521 "black_shulker_box", 522 "blue_shulker_box", 523 "brown_shulker_box", 524 "cyan_shulker_box", 525 "gray_shulker_box", 526 "green_shulker_box", 527 "light_blue_shulker_box", 528 "light_gray_shulker_box", 529 "lime_shulker_box", 530 "magenta_shulker_box", 531 "orange_shulker_box", 532 "pink_shulker_box", 533 "purple_shulker_box", 534 "red_shulker_box", 535 "shulker_box", 536 "white_shulker_box", 537 "yellow_shulker_box", 538 "smoker", 539 "structure_block", 540 "structure_void" 541 ) 542 .map(BlockTypes::get).filter(Objects::nonNull).forEach(tileEntityTypes::add); 543 } 544 return this.tileEntityTypes; 545 } 546 547 @Override 548 @NonNegative 549 public int getTileEntityCount(final @NonNull String world, final @NonNull BlockVector2 chunk) { 550 return Objects.requireNonNull(getWorld(world)). 551 getChunkAt(chunk.getBlockX(), chunk.getBlockZ()).getTileEntities().length; 552 } 553 554 @Override 555 public Set<BlockVector2> getChunkChunks(String world) { 556 Set<BlockVector2> chunks = super.getChunkChunks(world); 557 if (Bukkit.isPrimaryThread()) { 558 for (Chunk chunk : Objects.requireNonNull(Bukkit.getWorld(world)).getLoadedChunks()) { 559 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 560 chunks.add(loc); 561 } 562 } else { 563 final Semaphore semaphore = new Semaphore(1); 564 try { 565 semaphore.acquire(); 566 Bukkit.getScheduler().runTask(BukkitPlatform.getPlugin(BukkitPlatform.class), () -> { 567 for (Chunk chunk : Objects.requireNonNull(Bukkit.getWorld(world)).getLoadedChunks()) { 568 BlockVector2 loc = BlockVector2.at(chunk.getX() >> 5, chunk.getZ() >> 5); 569 chunks.add(loc); 570 } 571 semaphore.release(); 572 }); 573 semaphore.acquireUninterruptibly(); 574 } catch (final Exception e) { 575 e.printStackTrace(); 576 } 577 } 578 return chunks; 579 } 580 581}