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}