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