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