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.getTargetBlock(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 (!location.isPlotArea()) {
170            return;
171        }
172        //If entities are spawning... the chunk should be loaded?
173        Entity[] entities = event.getSpawnLocation().getChunk().getEntities();
174        if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) {
175            event.setShouldAbortSpawn(true);
176            event.setCancelled(true);
177            return;
178        }
179        CreatureSpawnEvent.SpawnReason reason = event.getReason();
180        switch (reason.toString()) {
181            case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> {
182                if (!area.isSpawnEggs()) {
183                    event.setShouldAbortSpawn(true);
184                    event.setCancelled(true);
185                    return;
186                }
187            }
188            case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SHEARED", "SILVERFISH_BLOCK", "ENDER_PEARL", "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN" -> {
189                if (!area.isMobSpawning()) {
190                    event.setShouldAbortSpawn(true);
191                    event.setCancelled(true);
192                    return;
193                }
194            }
195            case "BREEDING" -> {
196                if (!area.isSpawnBreeding()) {
197                    event.setShouldAbortSpawn(true);
198                    event.setCancelled(true);
199                    return;
200                }
201            }
202            case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER", "CUSTOM" -> {
203                if (!area.isSpawnCustom() && event.getType() != EntityType.ARMOR_STAND) {
204                    event.setShouldAbortSpawn(true);
205                    event.setCancelled(true);
206                    return;
207                }
208            }
209            case "SPAWNER" -> {
210                if (!area.isMobSpawnerSpawning()) {
211                    event.setShouldAbortSpawn(true);
212                    event.setCancelled(true);
213                    return;
214                }
215            }
216        }
217        Plot plot = location.getOwnedPlotAbs();
218        if (plot == null) {
219            EntityType type = event.getType();
220            // PreCreatureSpawnEvent **should** not be called for DROPPED_ITEM, just for the sake of consistency
221            if (type == EntityType.DROPPED_ITEM) {
222                if (Settings.Enabled_Components.KILL_ROAD_ITEMS) {
223                    event.setCancelled(true);
224                }
225                return;
226            }
227            if (!area.isMobSpawning()) {
228                if (type == EntityType.PLAYER) {
229                    return;
230                }
231                if (type.isAlive()) {
232                    event.setShouldAbortSpawn(true);
233                    event.setCancelled(true);
234                }
235            }
236            if (!area.isMiscSpawnUnowned() && !type.isAlive()) {
237                event.setShouldAbortSpawn(true);
238                event.setCancelled(true);
239            }
240            return;
241        }
242        if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) {
243            event.setShouldAbortSpawn(true);
244            event.setCancelled(true);
245        }
246    }
247
248    @EventHandler
249    public void onPlayerNaturallySpawnCreaturesEvent(PlayerNaturallySpawnCreaturesEvent event) {
250        if (Settings.Paper_Components.CANCEL_CHUNK_SPAWN) {
251            Location location = BukkitUtil.adapt(event.getPlayer().getLocation());
252            PlotArea area = location.getPlotArea();
253            if (area != null && !area.isMobSpawning()) {
254                event.setCancelled(true);
255            }
256        }
257    }
258
259    @EventHandler
260    public void onPreSpawnerSpawnEvent(PreSpawnerSpawnEvent event) {
261        if (Settings.Paper_Components.SPAWNER_SPAWN) {
262            Location location = BukkitUtil.adapt(event.getSpawnerLocation());
263            PlotArea area = location.getPlotArea();
264            if (area != null && !area.isMobSpawnerSpawning()) {
265                event.setCancelled(true);
266                event.setShouldAbortSpawn(true);
267            }
268        }
269    }
270
271    @EventHandler(priority = EventPriority.HIGHEST)
272    public void onBlockPlace(BlockPlaceEvent event) {
273        if (!Settings.Paper_Components.TILE_ENTITY_CHECK || !Settings.Enabled_Components.CHUNK_PROCESSOR) {
274            return;
275        }
276        if (!(event.getBlock().getState(false) instanceof TileState)) {
277            return;
278        }
279        final Location location = BukkitUtil.adapt(event.getBlock().getLocation());
280        final PlotArea plotArea = location.getPlotArea();
281        if (plotArea == null) {
282            return;
283        }
284        final int tileEntityCount = event.getBlock().getChunk().getTileEntities(false).length;
285        if (tileEntityCount >= Settings.Chunk_Processor.MAX_TILES) {
286            final PlotPlayer<?> plotPlayer = BukkitUtil.adapt(event.getPlayer());
287            plotPlayer.sendMessage(
288                    TranslatableCaption.of("errors.tile_entity_cap_reached"),
289                    TagResolver.resolver("amount", Tag.inserting(Component.text(Settings.Chunk_Processor.MAX_TILES)))
290            );
291            event.setCancelled(true);
292            event.setBuild(false);
293        }
294    }
295
296    /**
297     * Unsure if this will be any performance improvement over the spigot version,
298     * but here it is anyway :)
299     *
300     * @param event Paper's PlayerLaunchProjectileEvent
301     */
302    @EventHandler
303    public void onProjectileLaunch(PlayerLaunchProjectileEvent event) {
304        if (!Settings.Paper_Components.PLAYER_PROJECTILE) {
305            return;
306        }
307        Projectile entity = event.getProjectile();
308        ProjectileSource shooter = entity.getShooter();
309        if (!(shooter instanceof Player)) {
310            return;
311        }
312        Location location = BukkitUtil.adapt(entity.getLocation());
313        PlotArea area = location.getPlotArea();
314        if (area == null) {
315            return;
316        }
317        PlotPlayer<Player> pp = BukkitUtil.adapt((Player) shooter);
318        Plot plot = location.getOwnedPlot();
319
320        if (plot == null) {
321            if (!PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, ProjectilesFlag.class, true) && !pp.hasPermission(
322                    Permission.PERMISSION_ADMIN_PROJECTILE_ROAD
323            )) {
324                pp.sendMessage(
325                        TranslatableCaption.of("permission.no_permission_event"),
326                        TagResolver.resolver(
327                                "node",
328                                Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_ROAD)
329                        )
330                );
331                entity.remove();
332                event.setCancelled(true);
333            }
334        } else if (!plot.hasOwner()) {
335            if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) {
336                pp.sendMessage(
337                        TranslatableCaption.of("permission.no_permission_event"),
338                        TagResolver.resolver(
339                                "node",
340                                Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)
341                        )
342                );
343                entity.remove();
344                event.setCancelled(true);
345            }
346        } else if (!plot.isAdded(pp.getUUID())) {
347            if (!plot.getFlag(ProjectilesFlag.class)) {
348                if (!pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) {
349                    pp.sendMessage(
350                            TranslatableCaption.of("permission.no_permission_event"),
351                            TagResolver.resolver(
352                                    "node",
353                                    Tag.inserting(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)
354                            )
355                    );
356                    entity.remove();
357                    event.setCancelled(true);
358                }
359            }
360        }
361    }
362
363    @EventHandler
364    public void onAsyncTabCompletion(final AsyncTabCompleteEvent event) {
365        if (!Settings.Paper_Components.ASYNC_TAB_COMPLETION) {
366            return;
367        }
368        String buffer = event.getBuffer();
369        if (!(event.getSender() instanceof Player)) {
370            return;
371        }
372        if ((!event.isCommand() && !buffer.startsWith("/")) || buffer.indexOf(' ') == -1) {
373            return;
374        }
375        if (buffer.startsWith("/")) {
376            buffer = buffer.substring(1);
377        }
378        final String[] unprocessedArgs = buffer.split(Pattern.quote(" "));
379        if (unprocessedArgs.length == 1) {
380            return; // We don't do anything in this case
381        } else if (!Settings.Enabled_Components.TAB_COMPLETED_ALIASES
382                .contains(unprocessedArgs[0].toLowerCase(Locale.ENGLISH))) {
383            return;
384        }
385        final String[] args = new String[unprocessedArgs.length - 1];
386        System.arraycopy(unprocessedArgs, 1, args, 0, args.length);
387        try {
388            final PlotPlayer<?> player = BukkitUtil.adapt((Player) event.getSender());
389            final Collection<Command> objects = MainCommand.getInstance().tab(player, args, buffer.endsWith(" "));
390            if (objects == null) {
391                return;
392            }
393            final List<String> result = new ArrayList<>();
394            for (final com.plotsquared.core.command.Command o : objects) {
395                result.add(o.toString());
396            }
397            event.setCompletions(result);
398            event.setHandled(true);
399        } catch (final Exception ignored) {
400        }
401    }
402
403    @EventHandler(ignoreCancelled = true)
404    public void onBeaconEffect(final BeaconEffectEvent event) {
405        Block block = event.getBlock();
406        Location beaconLocation = BukkitUtil.adapt(block.getLocation());
407        Plot beaconPlot = beaconLocation.getPlot();
408
409        PlotArea area = beaconLocation.getPlotArea();
410        if (area == null) {
411            return;
412        }
413
414        Player player = event.getPlayer();
415        Location playerLocation = BukkitUtil.adapt(player.getLocation());
416
417        PlotPlayer<Player> plotPlayer = BukkitUtil.adapt(player);
418        Plot playerStandingPlot = playerLocation.getPlot();
419        if (playerStandingPlot == null) {
420            FlagContainer container = area.getRoadFlagContainer();
421            if (!getBooleanFlagValue(container, BeaconEffectsFlag.class, true) ||
422                    (beaconPlot != null && Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW)) {
423                event.setCancelled(true);
424            }
425            return;
426        }
427
428        FlagContainer container = playerStandingPlot.getFlagContainer();
429        boolean plotBeaconEffects = getBooleanFlagValue(container, BeaconEffectsFlag.class, true);
430        if (playerStandingPlot.equals(beaconPlot)) {
431            if (!plotBeaconEffects) {
432                event.setCancelled(true);
433            }
434            return;
435        }
436
437        if (!plotBeaconEffects || Settings.Enabled_Components.DISABLE_BEACON_EFFECT_OVERFLOW) {
438            event.setCancelled(true);
439        }
440    }
441
442    private boolean getBooleanFlagValue(
443            @NonNull FlagContainer container,
444            @NonNull Class<? extends BooleanFlag<?>> flagClass,
445            boolean defaultValue
446    ) {
447        BooleanFlag<?> flag = container.getFlag(flagClass);
448        return flag == null ? defaultValue : flag.getValue();
449    }
450
451}