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