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