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