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