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