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.google.inject.Inject;
022import com.plotsquared.bukkit.BukkitPlatform;
023import com.plotsquared.bukkit.player.BukkitPlayer;
024import com.plotsquared.bukkit.util.BukkitEntityUtil;
025import com.plotsquared.bukkit.util.BukkitUtil;
026import com.plotsquared.core.PlotSquared;
027import com.plotsquared.core.configuration.Settings;
028import com.plotsquared.core.listener.PlayerBlockEventType;
029import com.plotsquared.core.location.Location;
030import com.plotsquared.core.permissions.Permission;
031import com.plotsquared.core.player.PlotPlayer;
032import com.plotsquared.core.plot.Plot;
033import com.plotsquared.core.plot.PlotArea;
034import com.plotsquared.core.plot.PlotHandler;
035import com.plotsquared.core.plot.flag.implementations.DisablePhysicsFlag;
036import com.plotsquared.core.plot.flag.implementations.EntityChangeBlockFlag;
037import com.plotsquared.core.plot.flag.implementations.ExplosionFlag;
038import com.plotsquared.core.plot.flag.implementations.InvincibleFlag;
039import com.plotsquared.core.plot.flag.implementations.ProjectileChangeBlockFlag;
040import com.plotsquared.core.plot.world.PlotAreaManager;
041import com.plotsquared.core.util.EventDispatcher;
042import com.plotsquared.core.util.PlotFlagUtil;
043import com.sk89q.worldedit.bukkit.BukkitAdapter;
044import com.sk89q.worldedit.world.block.BlockType;
045import io.papermc.lib.PaperLib;
046import org.bukkit.Material;
047import org.bukkit.Particle;
048import org.bukkit.World;
049import org.bukkit.block.Block;
050import org.bukkit.entity.Ageable;
051import org.bukkit.entity.Boat;
052import org.bukkit.entity.Entity;
053import org.bukkit.entity.EntityType;
054import org.bukkit.entity.FallingBlock;
055import org.bukkit.entity.Player;
056import org.bukkit.entity.Projectile;
057import org.bukkit.entity.TNTPrimed;
058import org.bukkit.entity.Vehicle;
059import org.bukkit.event.Cancellable;
060import org.bukkit.event.EventHandler;
061import org.bukkit.event.EventPriority;
062import org.bukkit.event.Listener;
063import org.bukkit.event.entity.CreatureSpawnEvent;
064import org.bukkit.event.entity.EntityChangeBlockEvent;
065import org.bukkit.event.entity.EntityCombustByEntityEvent;
066import org.bukkit.event.entity.EntityDamageByEntityEvent;
067import org.bukkit.event.entity.EntityDamageEvent;
068import org.bukkit.event.entity.EntityExplodeEvent;
069import org.bukkit.event.entity.ExplosionPrimeEvent;
070import org.bukkit.event.vehicle.VehicleCreateEvent;
071import org.bukkit.metadata.FixedMetadataValue;
072import org.bukkit.metadata.MetadataValue;
073import org.bukkit.plugin.Plugin;
074import org.bukkit.projectiles.BlockProjectileSource;
075import org.bukkit.projectiles.ProjectileSource;
076import org.checkerframework.checker.nullness.qual.NonNull;
077
078import java.util.Iterator;
079import java.util.List;
080
081@SuppressWarnings("unused")
082public class EntityEventListener implements Listener {
083
084    private final BukkitPlatform platform;
085    private final PlotAreaManager plotAreaManager;
086    private final EventDispatcher eventDispatcher;
087    private float lastRadius;
088
089    @Inject
090    public EntityEventListener(
091            final @NonNull BukkitPlatform platform,
092            final @NonNull PlotAreaManager plotAreaManager,
093            final @NonNull EventDispatcher eventDispatcher
094    ) {
095        this.platform = platform;
096        this.plotAreaManager = plotAreaManager;
097        this.eventDispatcher = eventDispatcher;
098    }
099
100    @EventHandler(priority = EventPriority.HIGHEST)
101    public void onEntityCombustByEntity(EntityCombustByEntityEvent event) {
102        onEntityDamageByEntityCommon(event.getCombuster(), event.getEntity(), EntityDamageEvent.DamageCause.FIRE_TICK, event);
103    }
104
105    @EventHandler(priority = EventPriority.HIGHEST)
106    public void onEntityDamageByEntityEvent(EntityDamageByEntityEvent event) {
107        onEntityDamageByEntityCommon(event.getDamager(), event.getEntity(), event.getCause(), event);
108    }
109
110    private void onEntityDamageByEntityCommon(
111            final Entity damager,
112            final Entity victim,
113            final EntityDamageEvent.DamageCause cause,
114            final Cancellable event
115    ) {
116        Location location = BukkitUtil.adapt(damager.getLocation());
117        if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
118            return;
119        }
120        if (!BukkitEntityUtil.entityDamage(damager, victim, cause)) {
121            if (event.isCancelled()) {
122                if (victim instanceof Ageable ageable) {
123                    if (ageable.getAge() == -24000) {
124                        ageable.setAge(0);
125                        ageable.setAdult();
126                    }
127                }
128            }
129            event.setCancelled(true);
130        }
131    }
132
133    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
134    public void creatureSpawnEvent(CreatureSpawnEvent event) {
135        Entity entity = event.getEntity();
136        Location location = BukkitUtil.adapt(entity.getLocation());
137        PlotArea area = location.getPlotArea();
138        if (area == null) {
139            return;
140        }
141        // Armour-stands are handled elsewhere and should not be handled by area-wide entity-spawn options
142        if (entity.getType() == EntityType.ARMOR_STAND) {
143            return;
144        }
145        CreatureSpawnEvent.SpawnReason reason = event.getSpawnReason();
146        switch (reason.toString()) {
147            case "DISPENSE_EGG", "EGG", "OCELOT_BABY", "SPAWNER_EGG" -> {
148                if (!area.isSpawnEggs()) {
149                    event.setCancelled(true);
150                    return;
151                }
152            }
153            case "REINFORCEMENTS", "NATURAL", "MOUNT", "PATROL", "RAID", "SHEARED", "SILVERFISH_BLOCK", "ENDER_PEARL",
154                    "TRAP", "VILLAGE_DEFENSE", "VILLAGE_INVASION", "BEEHIVE", "CHUNK_GEN", "NETHER_PORTAL",
155                    "FROZEN", "SPELL", "DEFAULT" -> {
156                if (!area.isMobSpawning()) {
157                    event.setCancelled(true);
158                    return;
159                }
160            }
161            case "BREEDING", "DUPLICATION" -> {
162                if (!area.isSpawnBreeding()) {
163                    event.setCancelled(true);
164                    return;
165                }
166            }
167            case "CUSTOM" -> {
168                if (!area.isSpawnCustom()) {
169                    event.setCancelled(true);
170                    return;
171                }
172                // No need to clutter metadata if running paper
173                if (!PaperLib.isPaper()) {
174                    entity.setMetadata("ps_custom_spawned", new FixedMetadataValue(this.platform, true));
175                }
176                return; // Don't cancel if mob spawning is disabled
177            }
178            case "BUILD_IRONGOLEM", "BUILD_SNOWMAN", "BUILD_WITHER" -> {
179                if (!area.isSpawnCustom()) {
180                    event.setCancelled(true);
181                    return;
182                }
183            }
184            case "SPAWNER" -> {
185                if (!area.isMobSpawnerSpawning()) {
186                    event.setCancelled(true);
187                    return;
188                }
189            }
190        }
191        Plot plot = area.getOwnedPlotAbs(location);
192        if (plot == null) {
193            if (!area.isMobSpawning()) {
194                event.setCancelled(true);
195            }
196            return;
197        }
198        if (BukkitEntityUtil.checkEntity(entity, plot.getBasePlot(false))) {
199            event.setCancelled(true);
200        }
201    }
202
203    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
204    public void onEntityFall(EntityChangeBlockEvent event) {
205        if (event.getEntityType() != EntityType.FALLING_BLOCK) {
206            return;
207        }
208        Block block = event.getBlock();
209        World world = block.getWorld();
210        String worldName = world.getName();
211        if (!this.plotAreaManager.hasPlotArea(worldName)) {
212            return;
213        }
214        Location location = BukkitUtil.adapt(block.getLocation());
215        PlotArea area = location.getPlotArea();
216        if (area == null) {
217            return;
218        }
219        Plot plot = area.getOwnedPlotAbs(location);
220        if (plot == null || plot.getFlag(DisablePhysicsFlag.class)) {
221            event.setCancelled(true);
222            if (plot != null) {
223                if (block.getType().hasGravity()) {
224                    BlockEventListener.sendBlockChange(block.getLocation(), block.getBlockData());
225                }
226                plot.debug("Falling block event was cancelled because disable-physics = true");
227            }
228            return;
229        }
230        if (event.getTo().hasGravity()) {
231            Entity entity = event.getEntity();
232            List<MetadataValue> meta = entity.getMetadata("plot");
233            if (meta.isEmpty()) {
234                return;
235            }
236            Plot origin = (Plot) meta.get(0).value();
237            if (origin != null && !origin.equals(plot)) {
238                event.setCancelled(true);
239                entity.remove();
240            }
241        } else if (event.getTo() == Material.AIR) {
242            event.getEntity().setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
243        }
244    }
245
246    @EventHandler(priority = EventPriority.HIGH)
247    public void onDamage(EntityDamageEvent event) {
248        if (event.getEntityType() != EntityType.PLAYER) {
249            return;
250        }
251        Location location = BukkitUtil.adapt(event.getEntity().getLocation());
252        PlotArea area = location.getPlotArea();
253        if (area == null) {
254            return;
255        }
256        Plot plot = location.getOwnedPlot();
257        if (plot == null) {
258            if (PlotFlagUtil.isAreaRoadFlagsAndFlagEquals(area, InvincibleFlag.class, true)) {
259                event.setCancelled(true);
260            }
261            return;
262        }
263        if (plot.getFlag(InvincibleFlag.class)) {
264            plot.debug(event.getEntity().getName() + " could not take damage because invincible = true");
265            event.setCancelled(true);
266        }
267    }
268
269    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
270    public void onBigBoom(EntityExplodeEvent event) {
271        Location location = BukkitUtil.adapt(event.getLocation());
272        PlotArea area = location.getPlotArea();
273        boolean plotArea = location.isPlotArea();
274        if (!plotArea) {
275            if (!this.plotAreaManager.hasPlotArea(location.getWorldName())) {
276                return;
277            }
278            return;
279        }
280        Plot plot = area.getOwnedPlot(location);
281        if (plot != null) {
282            if (plot.getFlag(ExplosionFlag.class)) {
283                List<MetadataValue> meta = event.getEntity().getMetadata("plot");
284                Plot origin;
285                if (meta.isEmpty()) {
286                    origin = plot;
287                } else {
288                    origin = (Plot) meta.get(0).value();
289                }
290                if (this.lastRadius != 0) {
291                    List<Entity> nearby = event.getEntity().getNearbyEntities(this.lastRadius, this.lastRadius, this.lastRadius);
292                    for (Entity near : nearby) {
293                        if (near instanceof TNTPrimed || near.getType().equals(EntityType.MINECART_TNT)) {
294                            if (!near.hasMetadata("plot")) {
295                                near.setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
296                            }
297                        }
298                    }
299                    this.lastRadius = 0;
300                }
301                Iterator<Block> iterator = event.blockList().iterator();
302                while (iterator.hasNext()) {
303                    Block block = iterator.next();
304                    location = BukkitUtil.adapt(block.getLocation());
305                    if (!area.contains(location.getX(), location.getZ()) || !origin.equals(area.getOwnedPlot(location))) {
306                        iterator.remove();
307                    }
308                }
309                return;
310            } else {
311                plot.debug("Explosion was cancelled because explosion = false");
312            }
313        }
314        event.setCancelled(true);
315        //Spawn Explosion Particles when enabled in settings
316        if (Settings.General.ALWAYS_SHOW_EXPLOSIONS) {
317            event.getLocation().getWorld().spawnParticle(Particle.EXPLOSION_HUGE, event.getLocation(), 0);
318        }
319    }
320
321    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
322    public void onPeskyMobsChangeTheWorldLikeWTFEvent(EntityChangeBlockEvent event) {
323        Entity e = event.getEntity();
324        Material type = event.getBlock().getType();
325        Location location = BukkitUtil.adapt(event.getBlock().getLocation());
326        PlotArea area = location.getPlotArea();
327        if (area == null) {
328            return;
329        }
330        if (e instanceof FallingBlock) {
331            // allow falling blocks converting to blocks and vice versa
332            return;
333        } else if (e instanceof Boat) {
334            // allow boats destroying lily pads
335            if (type == Material.LILY_PAD) {
336                return;
337            }
338        } else if (e instanceof Player player) {
339            BukkitPlayer pp = BukkitUtil.adapt(player);
340            if (type.toString().equals("POWDER_SNOW")) {
341                // Burning player evaporating powder snow. Use same checks as
342                // trampling farmland
343                BlockType blockType = BukkitAdapter.asBlockType(type);
344                if (!this.eventDispatcher.checkPlayerBlockEvent(pp,
345                        PlayerBlockEventType.TRIGGER_PHYSICAL, location, blockType, true
346                )) {
347                    event.setCancelled(true);
348                }
349                return;
350            } else {
351                // already handled by other flags (mainly the 'use' flag):
352                // - player tilting big dripleaf by standing on it
353                // - player picking glow berries from cave vine
354                // - player trampling farmland
355                // - player standing on or clicking redstone ore
356                return;
357            }
358        } else if (e instanceof Projectile entity) {
359            // Exact same as the ProjectileHitEvent listener, except that we let
360            // the entity-change-block determine what to do with shooters that
361            // aren't players and aren't blocks
362            Plot plot = area.getPlot(location);
363            ProjectileSource shooter = entity.getShooter();
364            if (shooter instanceof Player) {
365                PlotPlayer<?> pp = BukkitUtil.adapt((Player) shooter);
366                if (plot == null) {
367                    if (area.isRoadFlags() && !area.getRoadFlag(ProjectileChangeBlockFlag.class) && !pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_UNOWNED)) {
368                        entity.remove();
369                        event.setCancelled(true);
370                    }
371                    return;
372                }
373                if (plot.isAdded(pp.getUUID()) || plot.getFlag(ProjectileChangeBlockFlag.class) || pp.hasPermission(Permission.PERMISSION_ADMIN_PROJECTILE_OTHER)) {
374                    return;
375                }
376                entity.remove();
377                event.setCancelled(true);
378                return;
379            }
380            if (!(shooter instanceof Entity) && shooter != null) {
381                if (plot == null) {
382                    entity.remove();
383                    event.setCancelled(true);
384                    return;
385                }
386                Location sLoc =
387                        BukkitUtil.adapt(((BlockProjectileSource) shooter).getBlock().getLocation());
388                if (!area.contains(sLoc.getX(), sLoc.getZ())) {
389                    entity.remove();
390                    event.setCancelled(true);
391                    return;
392                }
393                Plot sPlot = area.getOwnedPlotAbs(sLoc);
394                if (sPlot == null || !PlotHandler.sameOwners(plot, sPlot)) {
395                    entity.remove();
396                    event.setCancelled(true);
397                }
398                return;
399            }
400            // fall back to entity-change-block flag
401        }
402
403        Plot plot = area.getOwnedPlot(location);
404        if (plot != null && !plot.getFlag(EntityChangeBlockFlag.class)) {
405            plot.debug(e.getType() + " could not change block because entity-change-block = false");
406            event.setCancelled(true);
407        }
408    }
409
410    @EventHandler
411    public void onPrime(ExplosionPrimeEvent event) {
412        this.lastRadius = event.getRadius() + 1;
413    }
414
415    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
416    public void onVehicleCreate(VehicleCreateEvent event) {
417        Vehicle entity = event.getVehicle();
418        Location location = BukkitUtil.adapt(entity.getLocation());
419        PlotArea area = location.getPlotArea();
420        if (area == null) {
421            return;
422        }
423        Plot plot = area.getOwnedPlotAbs(location);
424        if (plot == null || BukkitEntityUtil.checkEntity(entity, plot)) {
425            entity.remove();
426            return;
427        }
428        if (Settings.Enabled_Components.KILL_ROAD_VEHICLES) {
429            entity.setMetadata("plot", new FixedMetadataValue((Plugin) PlotSquared.platform(), plot));
430        }
431    }
432
433}