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.listener;
020
021import com.google.inject.Inject;
022import com.plotsquared.bukkit.player.BukkitPlayer;
023import com.plotsquared.bukkit.util.BukkitUtil;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.configuration.Settings;
026import com.plotsquared.core.configuration.caption.TranslatableCaption;
027import com.plotsquared.core.database.DBFunc;
028import com.plotsquared.core.location.Location;
029import com.plotsquared.core.permissions.Permission;
030import com.plotsquared.core.player.PlotPlayer;
031import com.plotsquared.core.plot.Plot;
032import com.plotsquared.core.plot.PlotArea;
033import com.plotsquared.core.plot.flag.implementations.BlockBurnFlag;
034import com.plotsquared.core.plot.flag.implementations.BlockIgnitionFlag;
035import com.plotsquared.core.plot.flag.implementations.BreakFlag;
036import com.plotsquared.core.plot.flag.implementations.CoralDryFlag;
037import com.plotsquared.core.plot.flag.implementations.CropGrowFlag;
038import com.plotsquared.core.plot.flag.implementations.DisablePhysicsFlag;
039import com.plotsquared.core.plot.flag.implementations.DoneFlag;
040import com.plotsquared.core.plot.flag.implementations.ExplosionFlag;
041import com.plotsquared.core.plot.flag.implementations.GrassGrowFlag;
042import com.plotsquared.core.plot.flag.implementations.IceFormFlag;
043import com.plotsquared.core.plot.flag.implementations.IceMeltFlag;
044import com.plotsquared.core.plot.flag.implementations.InstabreakFlag;
045import com.plotsquared.core.plot.flag.implementations.KelpGrowFlag;
046import com.plotsquared.core.plot.flag.implementations.LeafDecayFlag;
047import com.plotsquared.core.plot.flag.implementations.LiquidFlowFlag;
048import com.plotsquared.core.plot.flag.implementations.MycelGrowFlag;
049import com.plotsquared.core.plot.flag.implementations.PlaceFlag;
050import com.plotsquared.core.plot.flag.implementations.RedstoneFlag;
051import com.plotsquared.core.plot.flag.implementations.SnowFormFlag;
052import com.plotsquared.core.plot.flag.implementations.SnowMeltFlag;
053import com.plotsquared.core.plot.flag.implementations.SoilDryFlag;
054import com.plotsquared.core.plot.flag.implementations.VineGrowFlag;
055import com.plotsquared.core.plot.flag.types.BlockTypeWrapper;
056import com.plotsquared.core.plot.flag.types.BooleanFlag;
057import com.plotsquared.core.plot.world.PlotAreaManager;
058import com.plotsquared.core.util.PlotFlagUtil;
059import com.plotsquared.core.util.task.TaskManager;
060import com.plotsquared.core.util.task.TaskTime;
061import com.sk89q.worldedit.WorldEdit;
062import com.sk89q.worldedit.bukkit.BukkitAdapter;
063import com.sk89q.worldedit.world.block.BlockType;
064import net.kyori.adventure.text.Component;
065import net.kyori.adventure.text.minimessage.tag.Tag;
066import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
067import org.bukkit.Bukkit;
068import org.bukkit.GameMode;
069import org.bukkit.Material;
070import org.bukkit.block.Block;
071import org.bukkit.block.BlockFace;
072import org.bukkit.block.BlockState;
073import org.bukkit.block.data.BlockData;
074import org.bukkit.block.data.type.Dispenser;
075import org.bukkit.block.data.type.Farmland;
076import org.bukkit.entity.Entity;
077import org.bukkit.entity.Fireball;
078import org.bukkit.entity.Player;
079import org.bukkit.entity.Projectile;
080import org.bukkit.event.EventHandler;
081import org.bukkit.event.EventPriority;
082import org.bukkit.event.Listener;
083import org.bukkit.event.block.BlockBreakEvent;
084import org.bukkit.event.block.BlockBurnEvent;
085import org.bukkit.event.block.BlockDamageEvent;
086import org.bukkit.event.block.BlockDispenseEvent;
087import org.bukkit.event.block.BlockExplodeEvent;
088import org.bukkit.event.block.BlockFadeEvent;
089import org.bukkit.event.block.BlockFormEvent;
090import org.bukkit.event.block.BlockFromToEvent;
091import org.bukkit.event.block.BlockGrowEvent;
092import org.bukkit.event.block.BlockIgniteEvent;
093import org.bukkit.event.block.BlockMultiPlaceEvent;
094import org.bukkit.event.block.BlockPhysicsEvent;
095import org.bukkit.event.block.BlockPistonExtendEvent;
096import org.bukkit.event.block.BlockPistonRetractEvent;
097import org.bukkit.event.block.BlockPlaceEvent;
098import org.bukkit.event.block.BlockRedstoneEvent;
099import org.bukkit.event.block.BlockSpreadEvent;
100import org.bukkit.event.block.CauldronLevelChangeEvent;
101import org.bukkit.event.block.EntityBlockFormEvent;
102import org.bukkit.event.block.LeavesDecayEvent;
103import org.bukkit.event.block.MoistureChangeEvent;
104import org.bukkit.event.block.SpongeAbsorbEvent;
105import org.bukkit.event.world.StructureGrowEvent;
106import org.bukkit.projectiles.BlockProjectileSource;
107import org.bukkit.util.Vector;
108import org.checkerframework.checker.nullness.qual.NonNull;
109
110import java.util.Iterator;
111import java.util.List;
112import java.util.Objects;
113import java.util.Set;
114import java.util.UUID;
115import java.util.stream.Collectors;
116import java.util.stream.Stream;
117
118import static org.bukkit.Tag.CORALS;
119import static org.bukkit.Tag.CORAL_BLOCKS;
120import static org.bukkit.Tag.WALL_CORALS;
121
122@SuppressWarnings("unused")
123public class BlockEventListener implements Listener {
124
125    private static final Set<Material> PISTONS = Set.of(
126            Material.PISTON,
127            Material.STICKY_PISTON
128    );
129    private static final Set<Material> PHYSICS_BLOCKS = Set.of(
130            Material.TURTLE_EGG,
131            Material.TURTLE_SPAWN_EGG
132    );
133    private static final Set<Material> SNOW = Stream.of(Material.values()) // needed as Tag.SNOW isn't present in 1.16.5
134            .filter(material -> material.name().contains("SNOW"))
135            .filter(Material::isBlock)
136            .collect(Collectors.toUnmodifiableSet());
137
138    private final PlotAreaManager plotAreaManager;
139    private final WorldEdit worldEdit;
140
141    @Inject
142    public BlockEventListener(final @NonNull PlotAreaManager plotAreaManager, final @NonNull WorldEdit worldEdit) {
143        this.plotAreaManager = plotAreaManager;
144        this.worldEdit = worldEdit;
145    }
146
147    public static void sendBlockChange(final org.bukkit.Location bloc, final BlockData data) {
148        TaskManager.runTaskLater(() -> {
149            String world = bloc.getWorld().getName();
150            int x = bloc.getBlockX();
151            int z = bloc.getBlockZ();
152            int distance = Bukkit.getViewDistance() * 16;
153
154            for (final PlotPlayer<?> player : PlotSquared.platform().playerManager().getPlayers()) {
155                Location location = player.getLocation();
156                if (location.getWorldName().equals(world)) {
157                    if (16 * Math.abs(location.getX() - x) / 16 > distance || 16 * Math.abs(location.getZ() - z) / 16 > distance) {
158                        continue;
159                    }
160                    ((BukkitPlayer) player).player.sendBlockChange(bloc, data);
161                }
162            }
163        }, TaskTime.ticks(3L));
164    }
165
166    @EventHandler
167    public void onRedstoneEvent(BlockRedstoneEvent event) {
168        Block block = event.getBlock();
169        Location location = BukkitUtil.adapt(block.getLocation());
170        PlotArea area = location.getPlotArea();
171        if (area == null) {
172            return;
173        }
174        Plot plot = location.getOwnedPlot();
175        if (plot == null) {
176            if (PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, RedstoneFlag.class, false)) {
177                event.setNewCurrent(0);
178            }
179            return;
180        }
181        if (!plot.getFlag(RedstoneFlag.class)) {
182            event.setNewCurrent(0);
183            plot.debug("Redstone event was cancelled because redstone = false");
184            return;
185        }
186        if (Settings.Redstone.DISABLE_OFFLINE) {
187            boolean disable = false;
188            if (!DBFunc.SERVER.equals(plot.getOwner())) {
189                if (plot.isMerged()) {
190                    disable = true;
191                    for (UUID owner : plot.getOwners()) {
192                        if (PlotSquared.platform().playerManager().getPlayerIfExists(owner) != null) {
193                            disable = false;
194                            break;
195                        }
196                    }
197                } else {
198                    disable = PlotSquared.platform().playerManager().getPlayerIfExists(plot.getOwnerAbs()) == null;
199                }
200            }
201            if (disable) {
202                for (UUID trusted : plot.getTrusted()) {
203                    if (PlotSquared.platform().playerManager().getPlayerIfExists(trusted) != null) {
204                        disable = false;
205                        break;
206                    }
207                }
208                if (disable) {
209                    event.setNewCurrent(0);
210                    plot.debug("Redstone event was cancelled because no trusted player was in the plot");
211                    return;
212                }
213            }
214        }
215        if (Settings.Redstone.DISABLE_UNOCCUPIED) {
216            for (final PlotPlayer<?> player : PlotSquared.platform().playerManager().getPlayers()) {
217                if (plot.equals(player.getCurrentPlot())) {
218                    return;
219                }
220            }
221            event.setNewCurrent(0);
222        }
223    }
224
225    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
226    public void onPhysicsEvent(BlockPhysicsEvent event) {
227        Block block = event.getBlock();
228        Location location = BukkitUtil.adapt(block.getLocation());
229        PlotArea area = location.getPlotArea();
230        if (area == null) {
231            return;
232        }
233        Plot plot = area.getOwnedPlotAbs(location);
234        if (plot == null) {
235            return;
236        }
237        if (event.getChangedType().hasGravity() && plot.getFlag(DisablePhysicsFlag.class)) {
238            event.setCancelled(true);
239            sendBlockChange(event.getBlock().getLocation(), event.getBlock().getBlockData());
240            plot.debug("Prevented block physics and resent block change because disable-physics = true");
241            return;
242        }
243        if (event.getChangedType() == Material.COMPARATOR) {
244            if (!plot.getFlag(RedstoneFlag.class)) {
245                event.setCancelled(true);
246                plot.debug("Prevented comparator update because redstone = false");
247            }
248            return;
249        }
250        if (PHYSICS_BLOCKS.contains(event.getChangedType())) {
251            if (plot.getFlag(DisablePhysicsFlag.class)) {
252                event.setCancelled(true);
253                plot.debug("Prevented block physics because disable-physics = true");
254            }
255            return;
256        }
257        if (Settings.Redstone.DETECT_INVALID_EDGE_PISTONS) {
258            if (PISTONS.contains(block.getType())) {
259                org.bukkit.block.data.Directional piston = (org.bukkit.block.data.Directional) block.getBlockData();
260                final BlockFace facing = piston.getFacing();
261                location = location.add(facing.getModX(), facing.getModY(), facing.getModZ());
262                Plot newPlot = area.getOwnedPlotAbs(location);
263                if (!plot.equals(newPlot)) {
264                    event.setCancelled(true);
265                    plot.debug("Prevented piston update because of invalid edge piston detection");
266                }
267            }
268        }
269    }
270
271    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
272    public void blockCreate(BlockPlaceEvent event) {
273        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
274        PlotArea area = location.getPlotArea();
275        if (area == null) {
276            return;
277        }
278        Player player = event.getPlayer();
279        BukkitPlayer pp = BukkitUtil.adapt(player);
280        Plot plot = area.getPlot(location);
281        if (plot != null) {
282            if (area.notifyIfOutsideBuildArea(pp, location.getY())) {
283                event.setCancelled(true);
284                pp.sendMessage(
285                        TranslatableCaption.of("height.height_limit"),
286                        TagResolver.builder()
287                                .tag("minheight", Tag.inserting(Component.text(area.getMinBuildHeight())))
288                                .tag("maxheight", Tag.inserting(Component.text(area.getMaxBuildHeight())))
289                                .build()
290                );
291                return;
292            }
293            if (!plot.hasOwner()) {
294                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_UNOWNED)) {
295                    pp.sendMessage(
296                            TranslatableCaption.of("permission.no_permission_event"),
297                            TagResolver.resolver(
298                                    "node",
299                                    Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_UNOWNED)
300                            )
301                    );
302                    event.setCancelled(true);
303                    return;
304                }
305            } else if (!plot.isAdded(pp.getUUID())) {
306                List<BlockTypeWrapper> place = plot.getFlag(PlaceFlag.class);
307                if (place != null) {
308                    Block block = event.getBlock();
309                    if (place.contains(
310                            BlockTypeWrapper.get(BukkitAdapter.asBlockType(block.getType())))) {
311                        return;
312                    }
313                }
314                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER)) {
315                    pp.sendMessage(
316                            TranslatableCaption.of("permission.no_permission_event"),
317                            TagResolver.resolver(
318                                    "node",
319                                    Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_OTHER)
320                            )
321                    );
322                    event.setCancelled(true);
323                    plot.debug(player.getName() + " could not place " + event.getBlock().getType()
324                            + " because of the place = false");
325                    return;
326                }
327            } else if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) {
328                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER)) {
329                    pp.sendMessage(
330                            TranslatableCaption.of("done.building_restricted")
331                    );
332                    event.setCancelled(true);
333                    return;
334                }
335            }
336            if (plot.getFlag(DisablePhysicsFlag.class)) {
337                Block block = event.getBlockPlaced();
338                if (block.getType().hasGravity()) {
339                    sendBlockChange(block.getLocation(), block.getBlockData());
340                    plot.debug(event.getBlock().getType()
341                            + " did not fall because of disable-physics = true");
342                }
343            }
344        } else if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_ROAD)) {
345            pp.sendMessage(
346                    TranslatableCaption.of("permission.no_permission_event"),
347                    TagResolver.resolver(
348                            "node",
349                            Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_ROAD)
350                    )
351            );
352            event.setCancelled(true);
353        }
354    }
355
356    @EventHandler(priority = EventPriority.LOWEST)
357    public void blockDestroy(BlockBreakEvent event) {
358        Player player = event.getPlayer();
359        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
360        PlotArea area = location.getPlotArea();
361        if (area == null) {
362            return;
363        }
364        Plot plot = area.getPlot(location);
365        if (plot != null) {
366            BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
367            // == rather than <= as we only care about the "ground level" not being destroyed
368            if (event.getBlock().getY() == area.getMinGenHeight()) {
369                if (!plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_GROUNDLEVEL)) {
370                    plotPlayer.sendMessage(
371                            TranslatableCaption.of("permission.no_permission_event"),
372                            TagResolver.resolver(
373                                    "node",
374                                    Tag.inserting(Permission.PERMISSION_ADMIN_DESTROY_GROUNDLEVEL)
375                            )
376                    );
377                    event.setCancelled(true);
378                    return;
379                }
380            } else if (area.notifyIfOutsideBuildArea(plotPlayer, location.getY())) {
381                event.setCancelled(true);
382                plotPlayer.sendMessage(
383                        TranslatableCaption.of("height.height_limit"),
384                        TagResolver.builder()
385                                .tag("minheight", Tag.inserting(Component.text(area.getMinBuildHeight())))
386                                .tag("maxheight", Tag.inserting(Component.text(area.getMaxBuildHeight())))
387                                .build()
388                );
389                return;
390            }
391            if (!plot.hasOwner()) {
392                if (!plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_UNOWNED, true)) {
393                    event.setCancelled(true);
394                }
395                return;
396            }
397            if (!plot.isAdded(plotPlayer.getUUID())) {
398                List<BlockTypeWrapper> destroy = plot.getFlag(BreakFlag.class);
399                Block block = event.getBlock();
400                final BlockType blockType = BukkitAdapter.asBlockType(block.getType());
401                for (final BlockTypeWrapper blockTypeWrapper : destroy) {
402                    if (blockTypeWrapper.accepts(blockType)) {
403                        return;
404                    }
405                }
406                if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_OTHER)) {
407                    return;
408                }
409                plotPlayer.sendMessage(
410                        TranslatableCaption.of("permission.no_permission_event"),
411                        TagResolver.resolver(
412                                "node",
413                                Tag.inserting(Permission.PERMISSION_ADMIN_DESTROY_OTHER)
414                        )
415                );
416                event.setCancelled(true);
417            } else if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) {
418                if (!plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER)) {
419                    plotPlayer.sendMessage(
420                            TranslatableCaption.of("done.building_restricted")
421                    );
422                    event.setCancelled(true);
423                    return;
424                }
425            }
426            return;
427        }
428        BukkitPlayer pp = BukkitUtil.adapt(player);
429        if (pp.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_ROAD)) {
430            return;
431        }
432        if (this.worldEdit != null && pp.getAttribute("worldedit")) {
433            if (player.getInventory().getItemInMainHand().getType() == Material
434                    .getMaterial(this.worldEdit.getConfiguration().wandItem)) {
435                return;
436            }
437        }
438        pp.sendMessage(
439                TranslatableCaption.of("permission.no_permission_event"),
440                TagResolver.resolver(
441                        "node",
442                        Tag.inserting(Permission.PERMISSION_ADMIN_DESTROY_ROAD)
443                )
444        );
445        event.setCancelled(true);
446    }
447
448    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
449    public void onBlockSpread(BlockSpreadEvent event) {
450        Block block = event.getBlock();
451        Location location = BukkitUtil.adapt(block.getLocation());
452        if (location.isPlotRoad()) {
453            event.setCancelled(true);
454            return;
455        }
456        PlotArea area = location.getPlotArea();
457        if (area == null) {
458            return;
459        }
460        Plot plot = area.getOwnedPlot(location);
461        if (plot == null) {
462            return;
463        }
464        switch (event.getSource().getType().toString()) {
465            case "GRASS_BLOCK":
466                if (!plot.getFlag(GrassGrowFlag.class)) {
467                    plot.debug("Grass could not grow because grass-grow = false");
468                    event.setCancelled(true);
469                }
470                break;
471            case "MYCELIUM":
472                if (!plot.getFlag(MycelGrowFlag.class)) {
473                    plot.debug("Mycelium could not grow because mycel-grow = false");
474                    event.setCancelled(true);
475                }
476                break;
477            case "WEEPING_VINES":
478            case "TWISTING_VINES":
479            case "CAVE_VINES":
480            case "VINE":
481            case "GLOW_BERRIES":
482                if (!plot.getFlag(VineGrowFlag.class)) {
483                    plot.debug("Vine could not grow because vine-grow = false");
484                    event.setCancelled(true);
485                }
486                break;
487            case "KELP":
488                if (!plot.getFlag(KelpGrowFlag.class)) {
489                    plot.debug("Kelp could not grow because kelp-grow = false");
490                    event.setCancelled(true);
491                }
492            case "BUDDING_AMETHYST":
493                if (!plot.getFlag(CropGrowFlag.class)) {
494                    plot.debug("Amethyst clusters could not grow because crop-grow = false");
495                    event.setCancelled(true);
496                }
497                break;
498        }
499    }
500
501    @EventHandler(priority = EventPriority.HIGHEST)
502    public void onCauldronEmpty(CauldronLevelChangeEvent event) {
503        Entity entity = event.getEntity();
504        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
505        PlotArea area = location.getPlotArea();
506        if (area == null) {
507            return;
508        }
509        Plot plot = area.getPlot(location);
510        // TODO Add flags for specific control over cauldron changes (rain, dripstone...)
511        switch (event.getReason()) {
512            case BANNER_WASH, ARMOR_WASH, EXTINGUISH -> {
513                if (entity instanceof Player player) {
514                    BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
515                    if (plot != null) {
516                        if (!plot.hasOwner()) {
517                            if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_INTERACT_UNOWNED)) {
518                                return;
519                            }
520                        } else if (!plot.isAdded(plotPlayer.getUUID())) {
521                            if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_INTERACT_OTHER)) {
522                                return;
523                            }
524                        } else {
525                            return;
526                        }
527                    } else {
528                        if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_INTERACT_ROAD)) {
529                            return;
530                        }
531                        if (this.worldEdit != null && plotPlayer.getAttribute("worldedit")) {
532                            if (player.getInventory().getItemInMainHand().getType() == Material
533                                    .getMaterial(this.worldEdit.getConfiguration().wandItem)) {
534                                return;
535                            }
536                        }
537                    }
538                }
539                if (event.getReason() == CauldronLevelChangeEvent.ChangeReason.EXTINGUISH && event.getEntity() != null) {
540                    event.getEntity().setFireTicks(0);
541                }
542                // Though the players fire ticks are modified,
543                // the cauldron water level change is cancelled and the event should represent that.
544                event.setCancelled(true);
545            }
546            default -> {
547                // Bucket empty, Bucket fill, Bottle empty, Bottle fill are already handled in PlayerInteract event
548                // Evaporation or Unknown reasons do not need to be cancelled as they are considered natural causes
549            }
550        }
551    }
552
553    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
554    public void onBlockForm(BlockFormEvent event) {
555        if (event instanceof EntityBlockFormEvent) {
556            return; // handled below
557        }
558        Block block = event.getBlock();
559        Location location = BukkitUtil.adapt(block.getLocation());
560        if (location.isPlotRoad()) {
561            event.setCancelled(true);
562            return;
563        }
564        PlotArea area = location.getPlotArea();
565        if (area == null) {
566            return;
567        }
568        Plot plot = area.getOwnedPlot(location);
569        if (plot == null) {
570            return;
571        }
572        if (!area.buildRangeContainsY(location.getY())) {
573            event.setCancelled(true);
574            return;
575        }
576        if (org.bukkit.Tag.SNOW.isTagged(event.getNewState().getType())) {
577            if (!plot.getFlag(SnowFormFlag.class)) {
578                plot.debug("Snow could not form because snow-form = false");
579                event.setCancelled(true);
580            }
581            return;
582        }
583        if (org.bukkit.Tag.ICE.isTagged(event.getNewState().getType())) {
584            if (!plot.getFlag(IceFormFlag.class)) {
585                plot.debug("Ice could not form because ice-form = false");
586                event.setCancelled(true);
587            }
588        }
589    }
590
591    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
592    public void onEntityBlockForm(EntityBlockFormEvent event) {
593        String world = event.getBlock().getWorld().getName();
594        if (!this.plotAreaManager.hasPlotArea(world)) {
595            return;
596        }
597        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
598        PlotArea area = location.getPlotArea();
599        if (area == null) {
600            return;
601        }
602        Plot plot = area.getOwnedPlot(location);
603        if (plot == null) {
604            event.setCancelled(true);
605            return;
606        }
607        Class<? extends BooleanFlag<?>> flag;
608        if (org.bukkit.Tag.SNOW.isTagged(event.getNewState().getType())) {
609            flag = SnowFormFlag.class;
610        } else if (org.bukkit.Tag.ICE.isTagged(event.getNewState().getType())) {
611            flag = IceFormFlag.class;
612        } else {
613            return;
614        }
615        boolean allowed = plot.getFlag(flag);
616        Entity entity = event.getEntity();
617        if (entity instanceof Player player) {
618            BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
619            if (!plot.isAdded(plotPlayer.getUUID())) {
620                if (allowed) {
621                    return; // player is not added but forming <flag> is allowed
622                }
623                plot.debug(String.format(
624                        "%s could not be formed because %s = false (entity is player)",
625                        event.getNewState().getType(),
626                        flag == SnowFormFlag.class ? "snow-form" : "ice-form"
627                ));
628                event.setCancelled(true); // player is not added and forming <flag> isn't allowed
629            }
630            return; // event is cancelled if not added and not allowed, otherwise forming <flag> is allowed
631        }
632        if (plot.hasOwner()) {
633            if (allowed) {
634                return;
635            }
636            plot.debug(String.format(
637                    "%s could not be formed because %s = false (entity is not player)",
638                    event.getNewState().getType(),
639                    flag == SnowFormFlag.class ? "snow-form" : "ice-form"
640            ));
641            event.setCancelled(true);
642        }
643    }
644
645    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
646    public void onBlockDamage(BlockDamageEvent event) {
647        Player player = event.getPlayer();
648        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
649        PlotArea area = location.getPlotArea();
650        if (area == null) {
651            return;
652        }
653        if (player.getGameMode() != GameMode.SURVIVAL) {
654            return;
655        }
656        Plot plot = area.getPlot(location);
657        if (plot != null) {
658            if (plot.getFlag(InstabreakFlag.class)) {
659                Block block = event.getBlock();
660                BlockBreakEvent call = new BlockBreakEvent(block, player);
661                Bukkit.getServer().getPluginManager().callEvent(call);
662                if (!call.isCancelled()) {
663                    event.getBlock().breakNaturally();
664                }
665            }
666            // == rather than <= as we only care about the "ground level" not being destroyed
667            if (location.getY() == area.getMinGenHeight()) {
668                event.setCancelled(true);
669                return;
670            }
671            if (!plot.hasOwner()) {
672                BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
673                if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_UNOWNED)) {
674                    return;
675                }
676                event.setCancelled(true);
677                return;
678            }
679            BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
680            if (!plot.isAdded(plotPlayer.getUUID())) {
681                List<BlockTypeWrapper> destroy = plot.getFlag(BreakFlag.class);
682                Block block = event.getBlock();
683                if (destroy
684                        .contains(BlockTypeWrapper.get(BukkitAdapter.asBlockType(block.getType())))
685                        || plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_OTHER)) {
686                    return;
687                }
688                plot.debug(player.getName() + " could not break " + block.getType()
689                        + " because it was not in the break flag");
690                event.setCancelled(true);
691                return;
692            }
693            return;
694        }
695        BukkitPlayer plotPlayer = BukkitUtil.adapt(player);
696        if (plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_ROAD)) {
697            return;
698        }
699        event.setCancelled(true);
700    }
701
702    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
703    public void onFade(BlockFadeEvent event) {
704        Block block = event.getBlock();
705        Location location = BukkitUtil.adapt(block.getLocation());
706        PlotArea area = location.getPlotArea();
707        if (area == null) {
708            return;
709        }
710        Plot plot = area.getOwnedPlot(location);
711        if (plot == null) {
712            event.setCancelled(true);
713            return;
714        }
715        Material blockType = block.getType();
716        if (org.bukkit.Tag.ICE.isTagged(blockType)) {
717            if (!plot.getFlag(IceMeltFlag.class)) {
718                plot.debug("Ice could not melt because ice-melt = false");
719                event.setCancelled(true);
720            }
721            return;
722        }
723        if (org.bukkit.Tag.SNOW.isTagged(blockType)) {
724            if (!plot.getFlag(SnowMeltFlag.class)) {
725                plot.debug("Snow could not melt because snow-melt = false");
726                event.setCancelled(true);
727            }
728            return;
729        }
730        if (blockType == Material.FARMLAND) {
731            if (!plot.getFlag(SoilDryFlag.class)) {
732                plot.debug("Soil could not dry because soil-dry = false");
733                event.setCancelled(true);
734            }
735            return;
736        }
737        if (CORAL_BLOCKS.isTagged(blockType) || CORALS.isTagged(blockType) || WALL_CORALS.isTagged(blockType)) {
738            if (!plot.getFlag(CoralDryFlag.class)) {
739                plot.debug("Coral could not dry because coral-dry = false");
740                event.setCancelled(true);
741            }
742        }
743    }
744
745    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
746    public void onMoistureChange(MoistureChangeEvent event) {
747        Block block = event.getBlock();
748        Location location = BukkitUtil.adapt(block.getLocation());
749        PlotArea area = location.getPlotArea();
750
751        if (area == null) {
752            return;
753        }
754
755        Plot plot = area.getOwnedPlot(location);
756
757        if (plot == null) {
758            event.setCancelled(true);
759            return;
760        }
761
762        if (block.getBlockData() instanceof Farmland farmland && event
763                .getNewState()
764                .getBlockData() instanceof Farmland newFarmland) {
765            int currentMoisture = farmland.getMoisture();
766            int newMoisture = newFarmland.getMoisture();
767
768            // farmland gets moisturizes
769            if (newMoisture > currentMoisture) {
770                return;
771            }
772
773            if (plot.getFlag(SoilDryFlag.class)) {
774                return;
775            }
776
777            plot.debug("Soil could not dry because soil-dry = false");
778            event.setCancelled(true);
779        }
780    }
781
782    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
783    public void onChange(BlockFromToEvent event) {
784        Block fromBlock = event.getBlock();
785
786        // Check liquid flow flag inside of origin plot too
787        final Location fromLocation = BukkitUtil.adapt(fromBlock.getLocation());
788        final PlotArea fromArea = fromLocation.getPlotArea();
789        if (fromArea != null) {
790            final Plot fromPlot = fromArea.getOwnedPlot(fromLocation);
791            if (fromPlot != null && fromPlot.getFlag(LiquidFlowFlag.class) == LiquidFlowFlag.FlowStatus.DISABLED && event
792                    .getBlock()
793                    .isLiquid()) {
794                fromPlot.debug("Liquid could not flow because liquid-flow = disabled");
795                event.setCancelled(true);
796                return;
797            }
798        }
799
800        Block toBlock = event.getToBlock();
801        Location toLocation = BukkitUtil.adapt(toBlock.getLocation());
802        PlotArea toArea = toLocation.getPlotArea();
803        if (toArea == null) {
804            if (fromBlock.getType() == Material.DRAGON_EGG && fromArea != null) {
805                event.setCancelled(true);
806            }
807            return;
808        }
809        if (!toArea.buildRangeContainsY(toLocation.getY())) {
810            event.setCancelled(true);
811            return;
812        }
813        Plot toPlot = toArea.getOwnedPlot(toLocation);
814
815        if (fromBlock.getType() == Material.DRAGON_EGG && fromArea != null) {
816            final Plot fromPlot = fromArea.getOwnedPlot(fromLocation);
817
818            if (fromPlot != null || toPlot != null) {
819                if ((fromPlot == null || !fromPlot.equals(toPlot)) && (toPlot == null || !toPlot.equals(fromPlot))) {
820                    event.setCancelled(true);
821                    return;
822                }
823            }
824        }
825
826        if (toPlot != null) {
827            if (!toArea.contains(fromLocation.getX(), fromLocation.getZ()) || !Objects.equals(
828                    toPlot,
829                    toArea.getOwnedPlot(fromLocation)
830            )) {
831                event.setCancelled(true);
832                return;
833            }
834            if (toPlot.getFlag(LiquidFlowFlag.class) == LiquidFlowFlag.FlowStatus.ENABLED && event.getBlock().isLiquid()) {
835                return;
836            }
837            if (toPlot.getFlag(DisablePhysicsFlag.class)) {
838                toPlot.debug(event.getBlock().getType() + " could not update because disable-physics = true");
839                event.setCancelled(true);
840                return;
841            }
842            if (toPlot.getFlag(LiquidFlowFlag.class) == LiquidFlowFlag.FlowStatus.DISABLED && event.getBlock().isLiquid()) {
843                toPlot.debug("Liquid could not flow because liquid-flow = disabled");
844                event.setCancelled(true);
845            }
846        } else if (!toArea.contains(fromLocation.getX(), fromLocation.getZ()) || !Objects.equals(
847                null,
848                toArea.getOwnedPlot(fromLocation)
849        )) {
850            event.setCancelled(true);
851        } else if (event.getBlock().isLiquid()) {
852            final org.bukkit.Location location = event.getBlock().getLocation();
853
854            /*
855                X = block location
856                A-H = potential plot locations
857               Z
858               ^
859               |    A B C
860               o    D X E
861               |    F G H
862               v
863                <-----O-----> x
864             */
865            if (BukkitUtil.adapt(location.clone().add(-1, 0, 1)  /* A */).getPlot() != null
866                    || BukkitUtil.adapt(location.clone().add(1, 0, 0)   /* B */).getPlot() != null
867                    || BukkitUtil.adapt(location.clone().add(1, 0, 1)   /* C */).getPlot() != null
868                    || BukkitUtil.adapt(location.clone().add(-1, 0, 0)  /* D */).getPlot() != null
869                    || BukkitUtil.adapt(location.clone().add(1, 0, 0)   /* E */).getPlot() != null
870                    || BukkitUtil.adapt(location.clone().add(-1, 0, -1) /* F */).getPlot() != null
871                    || BukkitUtil.adapt(location.clone().add(0, 0, -1)  /* G */).getPlot() != null
872                    || BukkitUtil.adapt(location.clone().add(1, 0, 1)   /* H */).getPlot() != null) {
873                event.setCancelled(true);
874            }
875        }
876    }
877
878
879    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
880    public void onGrow(BlockGrowEvent event) {
881        Block block = event.getBlock();
882        Location location = BukkitUtil.adapt(block.getLocation());
883
884        PlotArea area = location.getPlotArea();
885        if (area == null) {
886            return;
887        }
888
889        if (!area.buildRangeContainsY(location.getY())) {
890            event.setCancelled(true);
891            return;
892        }
893
894        Plot plot = location.getOwnedPlot();
895        if (plot == null || !plot.getFlag(CropGrowFlag.class)) {
896            if (plot != null) {
897                plot.debug("Crop grow event was cancelled because crop-grow = false");
898            }
899            event.setCancelled(true);
900        }
901    }
902
903    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
904    public void onBlockPistonExtend(BlockPistonExtendEvent event) {
905        Block block = event.getBlock();
906        Location location = BukkitUtil.adapt(block.getLocation());
907        BlockFace face = event.getDirection();
908        Vector relative = new Vector(face.getModX(), face.getModY(), face.getModZ());
909        PlotArea area = location.getPlotArea();
910        if (area == null) {
911            if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
912                return;
913            }
914            for (Block block1 : event.getBlocks()) {
915                Location bloc = BukkitUtil.adapt(block1.getLocation());
916                if (bloc.isPlotArea() || bloc
917                        .add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ())
918                        .isPlotArea()) {
919                    event.setCancelled(true);
920                    return;
921                }
922            }
923            if (location.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ()).isPlotArea()) {
924                // Prevent pistons from extending if they are: bordering a plot
925                // area, facing inside plot area, and not pushing any blocks
926                event.setCancelled(true);
927            }
928            return;
929        }
930        Plot plot = area.getOwnedPlot(location);
931        if (plot == null) {
932            event.setCancelled(true);
933            return;
934        }
935        for (Block block1 : event.getBlocks()) {
936            Location bloc = BukkitUtil.adapt(block1.getLocation());
937            Location newLoc = bloc.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ());
938            if (!area.contains(bloc.getX(), bloc.getZ()) || !area.contains(newLoc)) {
939                event.setCancelled(true);
940                return;
941            }
942            if (!plot.equals(area.getOwnedPlot(bloc)) || !plot.equals(area.getOwnedPlot(newLoc))) {
943                event.setCancelled(true);
944                return;
945            }
946            if (!area.buildRangeContainsY(bloc.getY()) || !area.buildRangeContainsY(newLoc.getY())) {
947                event.setCancelled(true);
948                return;
949            }
950        }
951        if (!plot.equals(area.getOwnedPlot(location.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ())))) {
952            // This branch is only necessary to prevent pistons from extending
953            // if they are: on a plot edge, facing outside the plot, and not
954            // pushing any blocks
955            event.setCancelled(true);
956        }
957    }
958
959    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
960    public void onBlockPistonRetract(BlockPistonRetractEvent event) {
961        Block block = event.getBlock();
962        Location location = BukkitUtil.adapt(block.getLocation());
963        BlockFace face = event.getDirection();
964        Vector relative = new Vector(face.getModX(), face.getModY(), face.getModZ());
965        PlotArea area = location.getPlotArea();
966        if (area == null) {
967            if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
968                return;
969            }
970            for (Block block1 : event.getBlocks()) {
971                Location bloc = BukkitUtil.adapt(block1.getLocation());
972                Location newLoc = bloc.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ());
973                if (bloc.isPlotArea() || newLoc.isPlotArea()) {
974                    event.setCancelled(true);
975                    return;
976                }
977            }
978            return;
979        }
980        Plot plot = area.getOwnedPlot(location);
981        if (plot == null) {
982            event.setCancelled(true);
983            return;
984        }
985        for (Block block1 : event.getBlocks()) {
986            Location bloc = BukkitUtil.adapt(block1.getLocation());
987            Location newLoc = bloc.add(relative.getBlockX(), relative.getBlockY(), relative.getBlockZ());
988            if (!area.contains(bloc.getX(), bloc.getZ()) || !area.contains(newLoc)) {
989                event.setCancelled(true);
990                return;
991            }
992            if (!plot.equals(area.getOwnedPlot(bloc)) || !plot.equals(area.getOwnedPlot(newLoc))) {
993                event.setCancelled(true);
994                return;
995            }
996            if (!area.buildRangeContainsY(bloc.getY()) || !area.buildRangeContainsY(newLoc.getY())) {
997                event.setCancelled(true);
998                return;
999            }
1000        }
1001    }
1002
1003    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1004    public void onBlockDispense(BlockDispenseEvent event) {
1005        if (!this.plotAreaManager.hasPlotArea(event.getBlock().getWorld().getName())) {
1006            return;
1007        }
1008        Material type = event.getItem().getType();
1009        switch (type.toString()) {
1010            case "SHULKER_BOX", "WHITE_SHULKER_BOX", "ORANGE_SHULKER_BOX", "MAGENTA_SHULKER_BOX", "LIGHT_BLUE_SHULKER_BOX",
1011                    "YELLOW_SHULKER_BOX", "LIME_SHULKER_BOX", "PINK_SHULKER_BOX", "GRAY_SHULKER_BOX", "LIGHT_GRAY_SHULKER_BOX",
1012                    "CYAN_SHULKER_BOX", "PURPLE_SHULKER_BOX", "BLUE_SHULKER_BOX", "BROWN_SHULKER_BOX", "GREEN_SHULKER_BOX",
1013                    "RED_SHULKER_BOX", "BLACK_SHULKER_BOX", "CARVED_PUMPKIN", "WITHER_SKELETON_SKULL", "FLINT_AND_STEEL",
1014                    "BONE_MEAL", "SHEARS", "GLASS_BOTTLE", "GLOWSTONE", "COD_BUCKET", "PUFFERFISH_BUCKET", "SALMON_BUCKET",
1015                    "TROPICAL_FISH_BUCKET", "AXOLOTL_BUCKET", "BUCKET", "WATER_BUCKET", "LAVA_BUCKET", "TADPOLE_BUCKET" -> {
1016                if (event.getBlock().getType() == Material.DROPPER) {
1017                    return;
1018                }
1019                BlockFace targetFace = ((Dispenser) event.getBlock().getBlockData()).getFacing();
1020                Location location = BukkitUtil.adapt(event.getBlock().getRelative(targetFace).getLocation());
1021                if (location.isPlotRoad()) {
1022                    event.setCancelled(true);
1023                    return;
1024                }
1025                PlotArea area = location.getPlotArea();
1026                if (area != null && !area.buildRangeContainsY(location.getY())) {
1027                    event.setCancelled(true);
1028                }
1029            }
1030        }
1031    }
1032
1033    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1034    public void onStructureGrow(StructureGrowEvent event) {
1035        if (!this.plotAreaManager.hasPlotArea(event.getWorld().getName())) {
1036            return;
1037        }
1038        List<org.bukkit.block.BlockState> blocks = event.getBlocks();
1039        if (blocks.isEmpty()) {
1040            return;
1041        }
1042        Location location = BukkitUtil.adapt(blocks.get(0).getLocation());
1043        PlotArea area = location.getPlotArea();
1044        if (area == null) {
1045            for (int i = blocks.size() - 1; i >= 0; i--) {
1046                location = BukkitUtil.adapt(blocks.get(i).getLocation());
1047                if (location.isPlotArea()) {
1048                    blocks.remove(i);
1049                }
1050            }
1051            return;
1052        } else {
1053            Plot origin = area.getOwnedPlot(location);
1054            if (origin == null) {
1055                event.setCancelled(true);
1056                return;
1057            }
1058            for (int i = blocks.size() - 1; i >= 0; i--) {
1059                location = BukkitUtil.adapt(blocks.get(i).getLocation());
1060                if (!area.contains(location.getX(), location.getZ())) {
1061                    blocks.remove(i);
1062                    continue;
1063                }
1064                Plot plot = area.getOwnedPlot(location);
1065                if (!Objects.equals(plot, origin)) {
1066                    event.getBlocks().remove(i);
1067                    continue;
1068                }
1069                if (!area.buildRangeContainsY(location.getY())) {
1070                    event.getBlocks().remove(i);
1071                }
1072            }
1073        }
1074        Plot origin = area.getPlot(location);
1075        if (origin == null) {
1076            event.setCancelled(true);
1077            return;
1078        }
1079        for (int i = blocks.size() - 1; i >= 0; i--) {
1080            location = BukkitUtil.adapt(blocks.get(i).getLocation());
1081            Plot plot = area.getOwnedPlot(location);
1082            /*
1083             * plot → the base plot of the merged area
1084             * origin → the plot where the event gets called
1085             */
1086
1087            // Are plot and origin different AND are both plots merged
1088            if (!Objects.equals(plot, origin) && (!plot.isMerged() && !origin.isMerged())) {
1089                event.getBlocks().remove(i);
1090            }
1091        }
1092    }
1093
1094    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1095    public void onBigBoom(BlockExplodeEvent event) {
1096        Block block = event.getBlock();
1097        Location location = BukkitUtil.adapt(block.getLocation());
1098        String world = location.getWorldName();
1099        if (!this.plotAreaManager.hasPlotArea(world)) {
1100            return;
1101        }
1102        PlotArea area = location.getPlotArea();
1103        if (area == null) {
1104            Iterator<Block> iterator = event.blockList().iterator();
1105            while (iterator.hasNext()) {
1106                location = BukkitUtil.adapt(iterator.next().getLocation());
1107                if (location.isPlotArea()) {
1108                    iterator.remove();
1109                }
1110            }
1111            return;
1112        }
1113        Plot plot = area.getOwnedPlot(location);
1114        if (plot == null || !plot.getFlag(ExplosionFlag.class)) {
1115            event.setCancelled(true);
1116            if (plot != null) {
1117                plot.debug("Explosion was cancelled because explosion = false");
1118            }
1119            return;
1120        }
1121        event.blockList().removeIf(blox -> !plot.equals(area.getOwnedPlot(BukkitUtil.adapt(blox.getLocation()))));
1122    }
1123
1124    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1125    public void onBlockBurn(BlockBurnEvent event) {
1126        Block block = event.getBlock();
1127        Location location = BukkitUtil.adapt(block.getLocation());
1128
1129        PlotArea area = location.getPlotArea();
1130        if (area == null) {
1131            return;
1132        }
1133
1134        Plot plot = location.getOwnedPlot();
1135        if (plot == null || !plot.getFlag(BlockBurnFlag.class)) {
1136            if (plot != null) {
1137                plot.debug("Block burning was cancelled because block-burn = false");
1138            }
1139            event.setCancelled(true);
1140        }
1141
1142    }
1143
1144    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1145    public void onBlockIgnite(BlockIgniteEvent event) {
1146        Player player = event.getPlayer();
1147        Entity ignitingEntity = event.getIgnitingEntity();
1148        Block block = event.getBlock();
1149        BlockIgniteEvent.IgniteCause igniteCause = event.getCause();
1150        Location location1 = BukkitUtil.adapt(block.getLocation());
1151        PlotArea area = location1.getPlotArea();
1152        if (area == null) {
1153            return;
1154        }
1155        if (igniteCause == BlockIgniteEvent.IgniteCause.LIGHTNING) {
1156            event.setCancelled(true);
1157            return;
1158        }
1159
1160        Plot plot = area.getOwnedPlot(location1);
1161        if (player != null) {
1162            BukkitPlayer pp = BukkitUtil.adapt(player);
1163            if (area.notifyIfOutsideBuildArea(pp, location1.getY())) {
1164                event.setCancelled(true);
1165                return;
1166            }
1167            if (plot == null) {
1168                if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, BlockIgnitionFlag.class, true) && !pp.hasPermission(
1169                        Permission.PERMISSION_ADMIN_BUILD_ROAD
1170                )) {
1171                    pp.sendMessage(
1172                            TranslatableCaption.of("permission.no_permission_event"),
1173                            TagResolver.resolver(
1174                                    "node",
1175                                    Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_ROAD)
1176                            )
1177                    );
1178                    event.setCancelled(true);
1179                }
1180            } else if (!plot.hasOwner()) {
1181                if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, BlockIgnitionFlag.class, true) && !pp.hasPermission(
1182                        Permission.PERMISSION_ADMIN_BUILD_UNOWNED
1183                )) {
1184                    pp.sendMessage(
1185                            TranslatableCaption.of("permission.no_permission_event"),
1186                            TagResolver.resolver(
1187                                    "node",
1188                                    Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_UNOWNED)
1189                            )
1190                    );
1191                    event.setCancelled(true);
1192                }
1193            } else if (!plot.isAdded(pp.getUUID())) {
1194                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER)) {
1195                    pp.sendMessage(
1196                            TranslatableCaption.of("permission.no_permission_event"),
1197                            TagResolver.resolver(
1198                                    "node",
1199                                    Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_OTHER)
1200                            )
1201                    );
1202                    event.setCancelled(true);
1203                }
1204            } else if (!plot.getFlag(BlockIgnitionFlag.class)) {
1205                event.setCancelled(true);
1206                plot.debug("Block ignition was cancelled because block-ignition = false");
1207            }
1208        } else {
1209            if (plot == null) {
1210                event.setCancelled(true);
1211                return;
1212            }
1213            if (ignitingEntity != null) {
1214                if (!plot.getFlag(BlockIgnitionFlag.class)) {
1215                    event.setCancelled(true);
1216                    plot.debug("Block ignition was cancelled because block-ignition = false");
1217                    return;
1218                }
1219                if (igniteCause == BlockIgniteEvent.IgniteCause.FIREBALL) {
1220                    if (ignitingEntity instanceof Fireball) {
1221                        Projectile fireball = (Projectile) ignitingEntity;
1222                        Location location = null;
1223                        if (fireball.getShooter() instanceof Entity shooter) {
1224                            location = BukkitUtil.adapt(shooter.getLocation());
1225                        } else if (fireball.getShooter() instanceof BlockProjectileSource) {
1226                            Block shooter =
1227                                    ((BlockProjectileSource) fireball.getShooter()).getBlock();
1228                            location = BukkitUtil.adapt(shooter.getLocation());
1229                        }
1230                        if (location != null && !plot.equals(location.getPlot())) {
1231                            event.setCancelled(true);
1232                        }
1233                    }
1234                }
1235
1236            } else if (event.getIgnitingBlock() != null) {
1237                Block ignitingBlock = event.getIgnitingBlock();
1238                Plot plotIgnited = BukkitUtil.adapt(ignitingBlock.getLocation()).getPlot();
1239                if (igniteCause == BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL && (
1240                        !plot.getFlag(BlockIgnitionFlag.class) || plotIgnited == null || !plotIgnited
1241                                .equals(plot)) || (igniteCause == BlockIgniteEvent.IgniteCause.SPREAD
1242                        || igniteCause == BlockIgniteEvent.IgniteCause.LAVA) && (
1243                        !plot.getFlag(BlockIgnitionFlag.class) || plotIgnited == null || !plotIgnited
1244                                .equals(plot))) {
1245                    event.setCancelled(true);
1246                }
1247            }
1248        }
1249    }
1250
1251    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
1252    public void onLeavesDecay(LeavesDecayEvent event) {
1253        Block block = event.getBlock();
1254        Location location = BukkitUtil.adapt(block.getLocation());
1255
1256        PlotArea area = location.getPlotArea();
1257        if (area == null) {
1258            return;
1259        }
1260
1261        Plot plot = location.getOwnedPlot();
1262        if (plot == null || !plot.getFlag(LeafDecayFlag.class)) {
1263            if (plot != null) {
1264                plot.debug("Leaf decaying was cancelled because leaf-decay = false");
1265            }
1266            event.setCancelled(true);
1267        }
1268
1269    }
1270
1271    @EventHandler(ignoreCancelled = true)
1272    public void onSpongeAbsorb(SpongeAbsorbEvent event) {
1273        Block sponge = event.getBlock();
1274        Location location = BukkitUtil.adapt(sponge.getLocation());
1275        PlotArea area = location.getPlotArea();
1276        List<org.bukkit.block.BlockState> blocks = event.getBlocks();
1277        if (area == null) {
1278            blocks.removeIf(block -> BukkitUtil.adapt(block.getLocation()).isPlotArea());
1279        } else {
1280            Plot origin = area.getOwnedPlot(location);
1281            blocks.removeIf(block -> {
1282                Location blockLocation = BukkitUtil.adapt(block.getLocation());
1283                if (!area.contains(blockLocation.getX(), blockLocation.getZ())) {
1284                    return true;
1285                }
1286                Plot plot = area.getOwnedPlot(blockLocation);
1287                if (!Objects.equals(plot, origin)) {
1288                    return true;
1289                }
1290                return !area.buildRangeContainsY(location.getY());
1291            });
1292        }
1293        if (blocks.isEmpty()) {
1294            // Cancel event so the sponge block doesn't turn into a wet sponge
1295            // if no water is being absorbed
1296            event.setCancelled(true);
1297        }
1298    }
1299
1300    /*
1301     * BlockMultiPlaceEvent is called unrelated to the BlockPlaceEvent itself and therefore doesn't respect the cancellation.
1302     */
1303    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
1304    public void onBlockMultiPlace(BlockMultiPlaceEvent event) {
1305        // Check if the generic block place event would be cancelled
1306        blockCreate(event);
1307        if (event.isCancelled()) {
1308            return;
1309        }
1310
1311        BukkitPlayer pp = BukkitUtil.adapt(event.getPlayer());
1312        Location placedLocation = BukkitUtil.adapt(event.getBlockReplacedState().getLocation());
1313        PlotArea area = placedLocation.getPlotArea();
1314        if (area == null) {
1315            return;
1316        }
1317        Plot plot = placedLocation.getPlot();
1318
1319        for (final BlockState state : event.getReplacedBlockStates()) {
1320            Location currentLocation = BukkitUtil.adapt(state.getLocation());
1321            if (!pp.hasPermission(
1322                    Permission.PERMISSION_ADMIN_BUILD_ROAD
1323            ) && !(Objects.equals(currentLocation.getPlot(), plot))) {
1324                pp.sendMessage(
1325                        TranslatableCaption.of("permission.no_permission_event"),
1326                        TagResolver.resolver("node", Tag.inserting(Permission.PERMISSION_ADMIN_BUILD_ROAD))
1327                );
1328                event.setCancelled(true);
1329                break;
1330            }
1331            if (pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_HEIGHT_LIMIT)) {
1332                continue;
1333            }
1334            if (currentLocation.getY() >= area.getMaxBuildHeight() || currentLocation.getY() < area.getMinBuildHeight()) {
1335                pp.sendMessage(
1336                        TranslatableCaption.of("height.height_limit"),
1337                        TagResolver.builder()
1338                                .tag("minheight", Tag.inserting(Component.text(area.getMinBuildHeight())))
1339                                .tag("maxheight", Tag.inserting(Component.text(area.getMaxBuildHeight())))
1340                                .build()
1341                );
1342                if (area.notifyIfOutsideBuildArea(pp, currentLocation.getY())) {
1343                    event.setCancelled(true);
1344                    break;
1345                }
1346            }
1347        }
1348
1349    }
1350
1351}