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