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}