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