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