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.destroystokyo.paper.event.block.BeaconEffectEvent;
022import com.destroystokyo.paper.event.block.BlockDestroyEvent;
023import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
024import com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent;
025import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent;
026import com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent;
027import com.destroystokyo.paper.event.entity.SlimePathfindEvent;
028import com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent;
029import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent;
030import com.google.inject.Inject;
031import com.plotsquared.bukkit.util.BukkitUtil;
032import com.plotsquared.core.PlotSquared;
033import com.plotsquared.core.command.Command;
034import com.plotsquared.core.command.MainCommand;
035import com.plotsquared.core.configuration.Settings;
036import com.plotsquared.core.configuration.caption.TranslatableCaption;
037import com.plotsquared.core.location.Location;
038import com.plotsquared.core.permissions.Permission;
039import com.plotsquared.core.player.PlotPlayer;
040import com.plotsquared.core.plot.Plot;
041import com.plotsquared.core.plot.PlotArea;
042import com.plotsquared.core.plot.PlotAreaType;
043import com.plotsquared.core.plot.flag.FlagContainer;
044import com.plotsquared.core.plot.flag.implementations.BeaconEffectsFlag;
045import com.plotsquared.core.plot.flag.implementations.DoneFlag;
046import com.plotsquared.core.plot.flag.implementations.FishingFlag;
047import com.plotsquared.core.plot.flag.implementations.ProjectilesFlag;
048import com.plotsquared.core.plot.flag.implementations.TileDropFlag;
049import com.plotsquared.core.plot.flag.types.BooleanFlag;
050import com.plotsquared.core.plot.world.PlotAreaManager;
051import com.plotsquared.core.util.PlotFlagUtil;
052import io.papermc.paper.event.entity.EntityMoveEvent;
053import io.papermc.paper.event.world.StructuresLocateEvent;
054import net.kyori.adventure.text.Component;
055import net.kyori.adventure.text.minimessage.tag.Tag;
056import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
057import org.bukkit.Chunk;
058import org.bukkit.NamespacedKey;
059import org.bukkit.block.Block;
060import org.bukkit.block.TileState;
061import org.bukkit.entity.Entity;
062import org.bukkit.entity.EntityType;
063import org.bukkit.entity.Player;
064import org.bukkit.entity.Projectile;
065import org.bukkit.entity.Slime;
066import org.bukkit.event.Cancellable;
067import org.bukkit.event.EventHandler;
068import org.bukkit.event.EventPriority;
069import org.bukkit.event.Listener;
070import org.bukkit.event.block.BlockPlaceEvent;
071import org.bukkit.event.entity.CreatureSpawnEvent;
072import org.bukkit.projectiles.ProjectileSource;
073import org.checkerframework.checker.nullness.qual.NonNull;
074
075import java.util.ArrayList;
076import java.util.Collection;
077import java.util.List;
078import java.util.Locale;
079import java.util.regex.Pattern;
080
081/**
082 * Events specific to Paper. Some toit nups here
083 */
084@SuppressWarnings("unused")
085public class PaperListener implements Listener {
086
087    private static final NamespacedKey ITEM = NamespacedKey.minecraft("item");
088    private static final NamespacedKey FISHING_BOBBER = NamespacedKey.minecraft("fishing_bobber");
089
090    private final PlotAreaManager plotAreaManager;
091    private Chunk lastChunk;
092
093    @Inject
094    public PaperListener(final @NonNull PlotAreaManager plotAreaManager) {
095        this.plotAreaManager = plotAreaManager;
096    }
097
098    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
099    public void onBlockDestroy(final BlockDestroyEvent event) {
100        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
101        PlotArea area = location.getPlotArea();
102        if (area == null) {
103            return;
104        }
105        Plot plot = area.getPlot(location);
106        if (plot != null) {
107            // Prevent dropping blocks which normally would not be dropped.
108            if (!event.willDrop()) {
109                return;
110            }
111            event.setWillDrop(plot.getFlag(TileDropFlag.class));
112        }
113    }
114
115    @EventHandler
116    public void onEntityPathfind(EntityPathfindEvent event) {
117        if (!Settings.Paper_Components.ENTITY_PATHING) {
118            return;
119        }
120        handleEntityMovement(event, event.getEntity().getLocation(), event.getLoc());
121    }
122
123    @EventHandler
124    public void onEntityPathfind(SlimePathfindEvent event) {
125        if (!Settings.Paper_Components.ENTITY_PATHING) {
126            return;
127        }
128        Slime slime = event.getEntity();
129
130        Block b = slime.getTargetBlockExact(4);
131        if (b == null) {
132            return;
133        }
134
135        handleEntityMovement(event, event.getEntity().getLocation(),  b.getLocation());
136    }
137
138    @EventHandler
139    public void onEntityMove(EntityMoveEvent event) {
140        if (!Settings.Paper_Components.ENTITY_MOVEMENT) {
141            return;
142        }
143        if (!event.hasExplicitlyChangedBlock()) {
144            return;
145        }
146        handleEntityMovement(event, event.getFrom(), event.getTo());
147    }
148
149    private static void handleEntityMovement(Cancellable event, org.bukkit.Location from, org.bukkit.Location target) {
150        Location toLoc = BukkitUtil.adapt(target);
151        Location fromLoc = BukkitUtil.adapt(from);
152        PlotArea tarea = toLoc.getPlotArea();
153        if (tarea == null) {
154            return;
155        }
156        PlotArea farea = fromLoc.getPlotArea();
157        if (farea == null) {
158            return;
159        }
160        if (tarea != farea) {
161            event.setCancelled(true);
162            return;
163        }
164        Plot tplot = toLoc.getPlot();
165        Plot fplot = fromLoc.getPlot();
166        if (tplot == null ^ fplot == null) {
167            event.setCancelled(true);
168            return;
169        }
170        if (tplot == null || tplot.getId().equals(fplot.getId())) {
171            return;
172        }
173        if (fplot.isMerged() && fplot.getConnectedPlots().contains(tplot)) {
174            return;
175        }
176        event.setCancelled(true);
177    }
178
179    @EventHandler
180    public void onPreCreatureSpawnEvent(PreCreatureSpawnEvent event) {
181        if (!Settings.Paper_Components.CREATURE_SPAWN) {
182            return;
183        }
184        Location location = BukkitUtil.adapt(event.getSpawnLocation());
185        PlotArea area = location.getPlotArea();
186        if (area == null) {
187            return;
188        }
189        // Armour-stands are handled elsewhere and should not be handled by area-wide entity-spawn options
190        if (event.getType() == EntityType.ARMOR_STAND) {
191            return;
192        }
193        // If entities are spawning... the chunk should be loaded?
194        Entity[] entities = event.getSpawnLocation().getChunk().getEntities();
195        if (entities.length >= Settings.Chunk_Processor.MAX_ENTITIES) {
196            event.setShouldAbortSpawn(true);
197            event.setCancelled(true);
198            return;
199        }
200        CreatureSpawnEvent.SpawnReason reason = event.getReason();
201        switch (reason.toString()) {
202            case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> {
203                if (!area.isSpawnEggs()) {
204                    event.setShouldAbortSpawn(true);
205                    event.setCancelled(true);
206                    return;
207                }
208            }
209            case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SILVERFISH_BLOCK", "ENDER_PEARL", "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN" -> {
210                if (!area.isMobSpawning()) {
211                    event.setShouldAbortSpawn(true);
212                    event.setCancelled(true);
213                    return;
214                }
215            }
216            case "BREEDING" -> {
217                if (!area.isSpawnBreeding()) {
218                    event.setShouldAbortSpawn(true);
219                    event.setCancelled(true);
220                    return;
221                }
222            }
223            case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER", "CUSTOM" -> {
224                if (!area.isSpawnCustom()) {
225                    event.setShouldAbortSpawn(true);
226                    event.setCancelled(true);
227                    return;
228                }
229            }
230            case "SPAWNER" -> {
231                if (!area.isMobSpawnerSpawning()) {
232                    event.setShouldAbortSpawn(true);
233                    event.setCancelled(true);
234                    return;
235                }
236            }
237        }
238        Plot plot = location.getOwnedPlotAbs();
239        if (plot == null) {
240            EntityType type = event.getType();
241            // PreCreatureSpawnEvent **should** not be called for DROPPED_ITEM, just for the sake of consistency
242            if (type.getKey().equals(ITEM)) {
243                if (Settings.Enabled_Components.KILL_ROAD_ITEMS) {
244                    event.setCancelled(true);
245                }
246                return;
247            }
248            if (!area.isMobSpawning()) {
249                if (type == EntityType.PLAYER) {
250                    return;
251                }
252                if (type.isAlive()) {
253                    event.setShouldAbortSpawn(true);
254                    event.setCancelled(true);
255                }
256            }
257            if (!area.isMiscSpawnUnowned() && !type.isAlive()) {
258                event.setShouldAbortSpawn(true);
259                event.setCancelled(true);
260            }
261            return;
262        }
263        if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) {
264            event.setShouldAbortSpawn(true);
265            event.setCancelled(true);
266        }
267    }
268
269    @EventHandler
270    public void onPlayerNaturallySpawnCreaturesEvent(PlayerNaturallySpawnCreaturesEvent event) {
271        if (Settings.Paper_Components.CANCEL_CHUNK_SPAWN) {
272            Location location = BukkitUtil.adapt(event.getPlayer().getLocation());
273            PlotArea area = location.getPlotArea();
274            if (area != null && !area.isMobSpawning()) {
275                event.setCancelled(true);
276            }
277        }
278    }
279
280    @EventHandler
281    public void onPreSpawnerSpawnEvent(PreSpawnerSpawnEvent event) {
282        if (Settings.Paper_Components.SPAWNER_SPAWN) {
283            Location location = BukkitUtil.adapt(event.getSpawnerLocation());
284            PlotArea area = location.getPlotArea();
285            if (area != null && !area.isMobSpawnerSpawning()) {
286                event.setCancelled(true);
287                event.setShouldAbortSpawn(true);
288            }
289        }
290    }
291
292    @EventHandler(priority = EventPriority.HIGHEST)
293    public void onBlockPlace(BlockPlaceEvent event) {
294        if (!Settings.Paper_Components.TILE_ENTITY_CHECK || !Settings.Enabled_Components.CHUNK_PROCESSOR) {
295            return;
296        }
297        if (!(event.getBlock().getState(false) instanceof TileState)) {
298            return;
299        }
300        final Location location = BukkitUtil.adapt(event.getBlock().getLocation());
301        final PlotArea plotArea = location.getPlotArea();
302        if (plotArea == null) {
303            return;
304        }
305        final int tileEntityCount = event.getBlock().getChunk().getTileEntities(false).length;
306        if (tileEntityCount >= Settings.Chunk_Processor.MAX_TILES) {
307            final PlotPlayer<?> plotPlayer = BukkitUtil.adapt(event.getPlayer());
308            plotPlayer.sendMessage(
309                    TranslatableCaption.of("errors.tile_entity_cap_reached"),
310                    TagResolver.resolver("amount", Tag.inserting(Component.text(Settings.Chunk_Processor.MAX_TILES)))
311            );
312            event.setCancelled(true);
313            event.setBuild(false);
314        }
315    }
316
317    /**
318     * Unsure if this will be any performance improvement over the spigot version,
319     * but here it is anyway :)
320     *
321     * @param event Paper's PlayerLaunchProjectileEvent
322     */
323    @EventHandler
324    public void onProjectileLaunch(PlayerLaunchProjectileEvent event) {
325        if (!Settings.Paper_Components.PLAYER_PROJECTILE) {
326            return;
327        }
328        Projectile entity = event.getProjectile();
329        ProjectileSource shooter = entity.getShooter();
330        if (!(shooter instanceof Player)) {
331            return;
332        }
333        Location location = BukkitUtil.adapt(entity.getLocation());
334        PlotArea area = location.getPlotArea();
335        if (area == null) {
336            return;
337        }
338        PlotPlayer<Player> pp = BukkitUtil.adapt((Player) shooter);
339        Plot plot = location.getOwnedPlot();
340
341        if (plot == null) {
342            if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, ProjectilesFlag.class, true) && !pp.hasPermission(
343                    Permission.PERMISSION_ADMIN_PROJECTILE_ROAD
344            )) {
345                pp.sendMessage(
346                        TranslatableCaption.of("permission.no_permission_event"),
347                        TagResolver.resolver(
348                                "node",
349                                Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_ROAD)
350                        )
351                );
352                entity.remove();
353                event.setCancelled(true);
354            }
355        } else if (!plot.hasOwner()) {
356            if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) {
357                pp.sendMessage(
358                        TranslatableCaption.of("permission.no_permission_event"),
359                        TagResolver.resolver(
360                                "node",
361                                Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)
362                        )
363                );
364                entity.remove();
365                event.setCancelled(true);
366            }
367        } else if (!plot.isAdded(pp.getUUID())) {
368            if (entity.getType().getKey().equals(FISHING_BOBBER)) {
369                if (plot.getFlag(FishingFlag.class)) {
370                    return;
371                }
372            }
373            if (!plot.getFlag(ProjectilesFlag.class)) {
374                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) {
375                    pp.sendMessage(
376                            TranslatableCaption.of("permission.no_permission_event"),
377                            TagResolver.resolver(
378                                    "node",
379                                    Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)
380                            )
381                    );
382                    entity.remove();
383                    event.setCancelled(true);
384                }
385            }
386        }
387    }
388
389    @EventHandler
390    public void onAsyncTabCompletion(final AsyncTabCompleteEvent event) {
391        if (!Settings.Paper_Components.ASYNC_TAB_COMPLETION) {
392            return;
393        }
394        String buffer = event.getBuffer();
395        if (!(event.getSender() instanceof Player)) {
396            return;
397        }
398        if ((!event.isCommand() && !buffer.startsWith("/")) || buffer.indexOf(' ') == -1) {
399            return;
400        }
401        if (buffer.startsWith("/")) {
402            buffer = buffer.substring(1);
403        }
404        final String[] unprocessedArgs = buffer.split(Pattern.quote(" "));
405        if (unprocessedArgs.length == 1) {
406            return; // We don't do anything in this case
407        } else if (!Settings.Enabled_Components.TAB_COMPLETED_ALIASES
408                .contains(unprocessedArgs[0].toLowerCase(Locale.ENGLISH))) {
409            return;
410        }
411        final String[] args = new String[unprocessedArgs.length - 1];
412        System.arraycopy(unprocessedArgs, 1, args, 0, args.length);
413        try {
414            final PlotPlayer<?> player = BukkitUtil.adapt((Player) event.getSender());
415            final Collection<Command> objects = MainCommand.getInstance().tab(player, args, buffer.endsWith(" "));
416            if (objects == null) {
417                return;
418            }
419            final List<String> result = new ArrayList<>();
420            for (final com.plotsquared.core.command.Command o : objects) {
421                result.add(o.toString());
422            }
423            event.setCompletions(result);
424            event.setHandled(true);
425        } catch (final Exception ignored) {
426        }
427    }
428
429    @EventHandler(ignoreCancelled = true)
430    public void onBeaconEffect(final BeaconEffectEvent event) {
431        Block block = event.getBlock();
432        Location beaconLocation = BukkitUtil.adapt(block.getLocation());
433        Plot beaconPlot = beaconLocation.getPlot();
434
435        PlotArea area = beaconLocation.getPlotArea();
436        if (area == null) {
437            return;
438        }
439
440        Player player = event.getPlayer();
441        Location playerLocation = BukkitUtil.adapt(player.getLocation());
442
443        PlotPlayer<Player> plotPlayer = BukkitUtil.adapt(player);
444        Plot playerStandingPlot = playerLocation.getPlot();
445        if (playerStandingPlot == null) {
446            FlagContainer container = area.getRoadFlagContainer();
447            if (!getBooleanFlagValue(container, BeaconEffectsFlag.class, true) ||
448                    (beaconPlot != null && Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW)) {
449                event.setCancelled(true);
450            }
451            return;
452        }
453
454        FlagContainer container = playerStandingPlot.getFlagContainer();
455        boolean plotBeaconEffects = getBooleanFlagValue(container, BeaconEffectsFlag.class, true);
456        if (playerStandingPlot.equals(beaconPlot)) {
457            if (!plotBeaconEffects) {
458                event.setCancelled(true);
459            }
460            return;
461        }
462
463        if (!plotBeaconEffects || Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW) {
464            event.setCancelled(true);
465        }
466    }
467
468    /**
469     * Don't let the server die when populating cartographers (villager offering maps) in classic plot worlds
470     * (as those don't generate POIs)
471     */
472    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
473    public void onStructuresLocate(StructuresLocateEvent event) {
474        if (!PlotSquared.get().getPlotAreaManager().hasPlotArea(event.getWorld().getName())) {
475            return;
476        }
477        final PlotArea area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(event.getWorld().getName());
478        if (area != null && area.getType() == PlotAreaType.NORMAL) {
479            event.setCancelled(true);
480        }
481    }
482
483    private boolean getBooleanFlagValue(
484            @NonNull FlagContainer container,
485            @NonNull Class<? extends BooleanFlag<?>> flagClass,
486            boolean defaultValue
487    ) {
488        BooleanFlag<?> flag = container.getFlag(flagClass);
489        return flag == null ? defaultValue : flag.getValue();
490    }
491
492}