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